import { LightningElement, api, track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
const SEARCH_DELAY = 300; // Wait 300 ms after user stops typing then, peform search
const KEY_ARROW_UP = 38;
const KEY_ARROW_DOWN = 40;
const KEY_ENTER = 13;
const VARIANT_LABEL_STACKED = 'label-stacked';
const VARIANT_LABEL_INLINE = 'label-inline';
const VARIANT_LABEL_HIDDEN = 'label-hidden';
const REGEX_SOSL_RESERVED = /(\?|&|\||!|\{|\}|\[|\]|\(|\)|\^|~|\*|:|"|\+|-|\\)/g;
const REGEX_EXTRA_TRAP = /(\$|\\)/g;
export default class LexLookup extends NavigationMixin(LightningElement) {
// Public properties
@api variant = VARIANT_LABEL_STACKED;
@api label = '';
@api required = false;
@api disabled = false;
@api placeholder = '';
@api isMultiEntry = false;
@api scrollAfterNItems = null;
@api newRecordOptions = [];
@api minSearchTermLength = 2;
@api isDisabledForDealerText = false;
@api accountValue = '';
// Template properties
searchResultsLocalState = [];
loading = false;
// Private properties
_errors = [];
_hasFocus = false;
_isDirty = false;
_searchTerm = '';
_cleanSearchTerm;
_cancelBlur = false;
_searchThrottlingTimeout;
_searchResults = [];
_defaultSearchResults = [];
_curSelection = [];
_focusedResultIndex = null;
// PUBLIC FUNCTIONS AND GETTERS/SETTERS
@api
set selection(initialSelection) {
if (initialSelection) {
this._curSelection = Array.isArray(initialSelection) ? initialSelection : [initialSelection];
this.processSelectionUpdate(false);
}
}
get selection() {
return this._curSelection;
}
@api
set errors(errors) {
this._errors = errors;
// Blur component if errors are passed
if (this._errors?.length > 0) {
this.blur();
}
}
get errors() {
return this._errors;
}
@api
get validity() {
return { valid: !this._errors || this._errors.length === 0 };
}
@api
get value() {
return this.getSelection();
}
@api
setSearchResults(results) {
// Reset the spinner
this.loading = false;
// Clone results before modifying them to avoid Locker restriction
let resultsLocal = JSON.parse(JSON.stringify(results));
// Remove selected items from search results
const selectedIds = this._curSelection.map((sel) => sel.id);
resultsLocal = resultsLocal.filter((result) => selectedIds.indexOf(result.id) === -1);
// Format results
const cleanSearchTerm = this._searchTerm.replace(REGEX_SOSL_RESERVED, '.?').replace(REGEX_EXTRA_TRAP, '\\$1');
const regex = new RegExp(`(${cleanSearchTerm})`, 'gi');
this._searchResults = resultsLocal.map((result) => {
// Format title and subtitle
if (this._searchTerm.length > 0) {
result.titleFormatted = result.title
? result.title.replace(regex, '$1')
: result.title;
result.subtitleFormatted = result.subtitle
? result.subtitle.replace(regex, '$1')
: result.subtitle;
} else {
result.titleFormatted = result.title;
result.subtitleFormatted = result.subtitle;
}
// Add icon if missing
if (typeof result.icon === 'undefined') {
result.icon = 'standard:default';
}
return result;
});
// Add local state and dynamic class to search results
this._focusedResultIndex = null;
const self = this;
this.searchResultsLocalState = this._searchResults.map((result, i) => {
return {
result,
state: {},
get classes() {
let cls = 'slds-media slds-media_center slds-listbox__option slds-listbox__option_entity';
if (result.subtitleFormatted) {
cls += ' slds-listbox__option_has-meta';
}
if (self._focusedResultIndex === i) {
cls += ' slds-has-focus';
}
return cls;
}
};
});
}
@api
getSelection() {
console.log('get selection:' +this._curSelection);
return this._curSelection;
}
@api
setDefaultResults(results) {
this._defaultSearchResults = [...results];
if (this._searchResults.length === 0) {
this.setSearchResults(this._defaultSearchResults);
}
}
@api
blur() {
this.template.querySelector('input')?.blur();
}
connectedCallback(){
console.log('LexLookup accountValue = ' + this.accountValue);
console.log('isDisabledForDealerText = ' + this.isDisabledForDealerText);
}
// INTERNAL FUNCTIONS
updateSearchTerm(newSearchTerm) {
this._searchTerm = newSearchTerm;
// Compare clean new search term with current one and abort if identical
const newCleanSearchTerm = newSearchTerm.trim().replace(REGEX_SOSL_RESERVED, '?').toLowerCase();
if (this._cleanSearchTerm === newCleanSearchTerm) {
return;
}
// Save clean search term
this._cleanSearchTerm = newCleanSearchTerm;
// Ignore search terms that are too small after removing special characters
if (newCleanSearchTerm.replace(/\?/g, '').length < this.minSearchTermLength) {
this.setSearchResults(this._defaultSearchResults);
return;
}
// Apply search throttling (prevents search if user is still typing)
if (this._searchThrottlingTimeout) {
clearTimeout(this._searchThrottlingTimeout);
}
// eslint-disable-next-line @lwc/lwc/no-async-operation
this._searchThrottlingTimeout = setTimeout(() => {
// Send search event if search term is long enougth
if (this._cleanSearchTerm.length >= this.minSearchTermLength) {
// Display spinner until results are returned
this.loading = true;
const searchEvent = new CustomEvent('search', {
detail: {
searchTerm: this._cleanSearchTerm,
rawSearchTerm: newSearchTerm,
selectedIds: this._curSelection.map((element) => element.id)
}
});
this.dispatchEvent(searchEvent);
}
this._searchThrottlingTimeout = null;
}, SEARCH_DELAY);
}
isSelectionAllowed() {
if (this.isMultiEntry) {
return true;
}
return !this.hasSelection();
}
hasSelection() {
return this._curSelection.length > 0;
}
processSelectionUpdate(isUserInteraction) {
// Reset search
this._cleanSearchTerm = '';
this._searchTerm = '';
this.setSearchResults([...this._defaultSearchResults]);
// Indicate that component was interacted with
this._isDirty = isUserInteraction;
// Blur input after single select lookup selection
if (!this.isMultiEntry && this.hasSelection()) {
this._hasFocus = false;
}
// If selection was changed by user, notify parent components
if (isUserInteraction) {
const selectedIds = this._curSelection.map((sel) => sel.id);
this.dispatchEvent(new CustomEvent('selectionchange', { detail: selectedIds }));
}
}
// EVENT HANDLING
handleInput(event) {
// Prevent action if selection is not allowed
if (!this.isSelectionAllowed()) {
return;
}
this.updateSearchTerm(event.target.value);
}
handleKeyDown(event) {
if (this._focusedResultIndex === null) {
this._focusedResultIndex = -1;
}
if (event.keyCode === KEY_ARROW_DOWN) {
// If we hit 'down', select the next item, or cycle over.
this._focusedResultIndex++;
if (this._focusedResultIndex >= this._searchResults.length) {
this._focusedResultIndex = 0;
}
event.preventDefault();
} else if (event.keyCode === KEY_ARROW_UP) {
// If we hit 'up', select the previous item, or cycle over.
this._focusedResultIndex--;
if (this._focusedResultIndex < 0) {
this._focusedResultIndex = this._searchResults.length - 1;
}
event.preventDefault();
} else if (event.keyCode === KEY_ENTER && this._hasFocus && this._focusedResultIndex >= 0) {
// If the user presses enter, and the box is open, and we have used arrows,
// treat this just like a click on the listbox item
const selectedId = this._searchResults[this._focusedResultIndex].id;
console.log('selectedid:'+selectedId);
this.template.querySelector(`[data-recordid="${selectedId}"]`).click();
event.preventDefault();
}
}
handleResultClick(event) {
const recordId = event.currentTarget.dataset.recordid;
// Save selection
const selectedItem = this._searchResults.find((result) => result.id === recordId);
if (!selectedItem) {
return;
}
const newSelection = [...this._curSelection];
newSelection.push(selectedItem);
this._curSelection = newSelection;
// Process selection update
this.processSelectionUpdate(true);
}
handleComboboxMouseDown(event) {
const mainButton = 0;
if (event.button === mainButton) {
this._cancelBlur = true;
}
}
handleComboboxMouseUp() {
this._cancelBlur = false;
// Re-focus to text input for the next blur event
this.template.querySelector('input').focus();
}
handleFocus() {
// Prevent action if selection is not allowed
if (!this.isSelectionAllowed()) {
return;
}
this._hasFocus = true;
this._focusedResultIndex = null;
}
handleBlur() {
// Prevent action if selection is either not allowed or cancelled
if (!this.isSelectionAllowed() || this._cancelBlur) {
return;
}
const blurEvent = new CustomEvent('blur', {
detail: {}
});
this.dispatchEvent(blurEvent);
this._hasFocus = false;
if(!this.hasSelection()){
this._searchTerm = '';
}
}
handleRemoveSelectedItem(event) {
if (this.disabled) {
return;
}
const recordId = event.currentTarget.name;
this._curSelection = this._curSelection.filter((item) => item.id !== recordId);
// Process selection update
this.processSelectionUpdate(true);
}
handleClearSelection() {
this._curSelection = [];
this._hasFocus = false;
this.accountValue = '';
// Process selection update
this.processSelectionUpdate(true);
}
handleNewRecordClick(event) {
const objectApiName = event.currentTarget.dataset.sobject;
const selection = this.newRecordOptions.find((option) => option.value === objectApiName);
const preNavigateCallback = selection.preNavigateCallback
? selection.preNavigateCallback
: () => Promise.resolve();
preNavigateCallback(selection).then(() => {
this[NavigationMixin.Navigate]({
type: 'standard__objectPage',
attributes: {
objectApiName,
actionName: 'new'
},
state: {
defaultFieldValues: selection.defaults
}
});
});
}
// STYLE EXPRESSIONS
get isSingleEntry() {
return !this.isMultiEntry;
}
get isListboxOpen() {
const isSearchTermValid = this._cleanSearchTerm && this._cleanSearchTerm.length >= this.minSearchTermLength;
return (
this._hasFocus &&
this.isSelectionAllowed() &&
(isSearchTermValid || this.hasResults || this.newRecordOptions?.length > 0)
);
}
get hasResults() {
return this._searchResults.length > 0;
}
get getFormElementClass() {
return this.variant === VARIANT_LABEL_INLINE
? 'slds-form-element slds-form-element_horizontal'
: 'slds-form-element';
}
get getLabelClass() {
return this.variant === VARIANT_LABEL_HIDDEN
? 'slds-form-element__label slds-assistive-text'
: 'slds-form-element__label';
}
get getContainerClass() {
let css = 'slds-combobox_container ';
if (this._errors.length > 0) {
css += 'has-custom-error';
}
return css;
}
get getDropdownClass() {
let css = 'slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click ';
if (this.isListboxOpen) {
css += 'slds-is-open';
}
return css;
}
get getInputClass() {
let css = 'slds-input slds-combobox__input has-custom-height ';
if (this._hasFocus && this.hasResults) {
css += 'slds-has-focus ';
}
if (this._errors.length > 0 || (this._isDirty && this.required && !this.hasSelection())) {
css += 'has-custom-error ';
}
if (!this.isMultiEntry) {
css += 'slds-combobox__input-value ' + (this.hasSelection() ? 'has-custom-border' : '');
}
return css;
}
get getComboboxClass() {
let css = 'slds-combobox__form-element slds-input-has-icon ';
if (this.isMultiEntry) {
css += 'slds-input-has-icon_right';
} else {
css += this.hasSelection() ? 'slds-input-has-icon_left-right' : 'slds-input-has-icon_right';
}
return css;
}
get getSearchIconClass() {
let css = 'slds-input__icon slds-input__icon_right ';
if (!this.isMultiEntry) {
css += this.hasSelection() ? 'slds-hide' : '';
}
return css;
}
get getClearSelectionButtonClass() {
return (
'slds-button slds-button_icon slds-input__icon slds-input__icon_right ' +
(this.hasSelection() ? '' : 'slds-hide')
);
}
get getSelectIconName() {
if(this._curSelection[0])
console.log('this._curSelection[0].icon = ' + this._curSelection[0].icon);
return this.hasSelection() ? this._curSelection[0].icon : 'standard:default';
}
get getSelectIconClass() {
return 'slds-combobox__input-entity-icon ' + (this.hasSelection() ? '' : 'slds-hide');
}
get getInputValue() {
if (this.isMultiEntry) {
return this._searchTerm;
}
if(this.accountValue != ''){
return this.accountValue;
}
return this.hasSelection() ? this._curSelection[0].title : this._searchTerm;
}
get getInputTitle() {
if (this.isMultiEntry) {
return '';
}
return this.hasSelection() ? this._curSelection[0].title : '';
}
get getListboxClass() {
return (
'slds-dropdown ' +
(this.scrollAfterNItems ? `slds-dropdown_length-with-icon-${this.scrollAfterNItems} ` : '') +
'slds-dropdown_fluid'
);
}
get isInputReadonly() {
if (this.isMultiEntry) {
return false;
}
return this.hasSelection();
}
}