/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { LitElement, html, CSSResultArray, TemplateResult, PropertyValues, } from 'lit'; import { customElement, query, state, property } from 'lit/decorators.js'; import { styles } from './nile-chip.css'; import { classMap } from 'lit/directives/class-map.js'; import { HasSlotController } from '../internal/slot'; import NileElement, { NileFormControl } from '../internal/nile-element'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { VisibilityManager } from '../utilities/visibility-manager.js'; interface CustomEventDetail { value: string; } @customElement('nile-chip') export class NileChip extends NileElement { public static get styles(): CSSResultArray { return [styles]; } private readonly hasSlotController = new HasSlotController( this, 'help-text', 'label' ); @state() tags: string[] = []; @state() inputValue: string = ''; @state() isDropdownOpen: boolean = false; @state() tooltips: (string | null)[] = []; @state() private chipFocusIndex: number | null = null; @query('nile-auto-complete') autoComplete!: any; /** Sets the input to a warning state, changing its visual appearance. */ @property({ type: Boolean }) warning = false; @property({ type: Boolean }) noAutoComplete = false; /** Sets the input to an error state, changing its visual appearance. */ @property({ type: Boolean }) error = false; /** Sets the input to a success state, changing its visual appearance. */ @property({ type: Boolean }) success = false; /** Disables the duplicate entries. */ @property({ type: Boolean }) noDuplicates = false; /** The input's label. If you need to display HTML, use the `label` slot instead. */ @property() label = ''; @property({ type: String }) tagVariant: string = ''; // can be '', 'normal', 'success', etc. /** Adds a clear button when the input is not empty. */ @property({ type: Boolean }) acceptUserInput = false; /** When true, adds a chip when the input loses focus or when clicking outside the component. Only works when acceptUserInput is true. */ @property({ type: Boolean }) addOnBlur = false; /** Adds a clear button when the input is not empty. */ @property({ type: Boolean }) clearable = false; /** Placeholder text to show as a hint when the input is empty. */ @property() placeholder = 'type here...'; /** Makes the input readonly. */ @property({ type: Boolean, reflect: true }) readonly = false; /** Disables the input. */ @property({ type: Boolean, reflect: true }) disabled = false; /** * When true, the dropdown menu will be appended to the document body instead of the parent container. * This is useful when the parent has overflow: hidden, clip-path, or transform applied. */ @property({ type: Boolean, reflect: true }) portal = false; // AUTO-COMPLETE-OPTIONS /** Virtual scroll in dropdown options. */ @property({ type: Boolean }) enableVirtualScroll = false; @property({ type: Array }) autoCompleteOptions: any[] = []; @property({ type: Array }) filteredAutoCompleteOptions: any[] = []; @property({ type: Array }) value: any[] = []; @property({ type: Boolean }) noWrap: boolean = false; @property({ type: Boolean }) loading: boolean = false; @property({ type: Array }) errorIndexes: number[] = []; @property({ attribute: 'help-text', reflect: true }) helpText = ''; @property({ attribute: 'error-message', reflect: true }) errorMessage = ''; @property({ attribute:false}) filterFunction: (item:string,searchedValue:string)=>boolean = (item:string,searchedValue:string)=>item.toLowerCase().includes(searchedValue.toLowerCase()); @property({ attribute:false}) renderItemFunction: (item:any)=>string = (item:any)=>item; private visibilityManager?: VisibilityManager; @property({ type: Boolean, reflect: true }) enableVisibilityEffect = false; @property({ type: Boolean, reflect: true }) enableTabClose = false; @property({ type: Boolean }) showTooltip: boolean = false; @property({ type: Boolean }) enableTagDelete = false; @property({ type: Boolean, reflect: true }) required = false; @property({ reflect: true, converter: { fromAttribute: value => value !== 'false', toAttribute: value => (value ? '' : 'false'), }, }) openDropdownOnFocus = true; @property({ type: Boolean, reflect: true, attribute: true }) noDropdownClose = false; private isSelectingFromDropdown = false; private resetDropdownState() { this.isSelectingFromDropdown = false; } protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); if (changedProperties.has('autoCompleteOptions')) { let options = this.autoCompleteOptions; if (typeof options === 'string') { try { options = JSON.parse(options); } catch (e) { options = []; } } this.filteredAutoCompleteOptions = Array.isArray(options) ? [...options] : []; if (this.noDuplicates) { this.filteredAutoCompleteOptions = this.filteredAutoCompleteOptions.filter( option => !this.value.includes(option) ); } } if (changedProperties.has('value')){ this.tags = [...this.value]; this.onTagsChanged(); } if (changedProperties.has('tags')){ this.onTagsChanged(); } } protected async firstUpdated(_changed: PropertyValues) { await this.updateComplete; const inputTarget = this.noAutoComplete ? this.renderRoot.querySelector('nile-input')?.input : this.autoComplete?.inputElement?.input || this.autoComplete?.inputElement; this.visibilityManager = new VisibilityManager({ host: this, target: inputTarget, enableVisibilityEffect: this.enableVisibilityEffect, enableTabClose: this.enableTabClose, isOpen: () => this.isDropdownOpen, onAnchorOutOfView: () => { this.isDropdownOpen = false; if (this.autoComplete) { this.autoComplete.isDropdownOpen = false; } this.emit('nile-visibility-change', { visible: false, reason: 'anchor-out-of-view', }); }, onDocumentHidden: () => { this.isDropdownOpen = false; if (this.autoComplete) { this.autoComplete.isDropdownOpen = false; } this.emit('nile-visibility-change', { visible: false, reason: 'document-hidden', }); }, emit: (event, detail) => this.emit(`nile-${event}`, detail), }); } private handleDocumentClick = (event: MouseEvent) => { const path = event.composedPath(); if (this.isSelectingFromDropdown) { this.resetDropdownState(); return; } if (!path.includes(this)) { if (this.addOnBlur && this.acceptUserInput && this.inputValue) { this.addChipFromInputValue(); } this.isDropdownOpen = false; } }; connectedCallback() { super.connectedCallback(); document.addEventListener('click', this.handleDocumentClick); if (this.noDuplicates) { this.filteredAutoCompleteOptions = this.filteredAutoCompleteOptions.filter( option => !this.value.includes(option) ); } this.emit('nile-init'); } disconnectedCallback() { super.disconnectedCallback(); this.visibilityManager?.cleanup(); document.removeEventListener('click', this.handleDocumentClick); this.emit('nile-destroy'); } private markDropdownSelectStart(event: MouseEvent) { this.isSelectingFromDropdown = true; } render() { // Check if slots are present const hasLabelSlot = this.hasSlotController.test('label'); const hasHelpTextSlot = this.hasSlotController.test('help-text'); // Check if label and help text are present const hasLabel = this.label ? true : !!hasLabelSlot; const hasHelpText = this.helpText ? true : false; const hasErrorMessage = this.errorMessage ? true : false; return html`