import '@digital-realty/ix-button/ix-button.js'; import '@digital-realty/ix-date/ix-date.js'; import '@digital-realty/ix-date-next/ix-date-next.js'; import '@digital-realty/ix-icon-button/ix-icon-button.js'; import '@digital-realty/ix-icon/ix-icon.js'; import '@digital-realty/ix-select/ix-select.js'; import { format, formatDate } from 'date-fns/format.js'; import { LitElement, html, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import type { Column, DataType, FilterOperator } from '../IxGrid.js'; import { IxGridViewStyles } from '../grid-view-styles.js'; import { copy } from '../ix-grid-copy.js'; import { IxGridRowFilterStyles } from './grid-row-filter-styles.js'; export interface Filter { columnField: string; operatorValue: string; dataType?: DataType; value: string; } @customElement('ix-grid-row-filter') export class IxGridRowFilter extends LitElement { static readonly styles = [IxGridViewStyles, IxGridRowFilterStyles]; @property({ type: Array }) columns: Column[] = []; @property({ type: Number }) filterValueChangeDebounceTime: number = 300; @property({ type: Boolean }) readParamsFromURL = false; @property({ type: Boolean }) useNewDatePicker = false; @property({ type: String }) maxDate: string = formatDate( new Date(), 'yyyy-MM-dd' ); @state() private isDropdownVisible: boolean = false; @state() private filters: Filter[] = []; @state() private filterableColumns: Column[] = []; @state() private filterColumns: string[] = []; @state() private activeFilters: Filter[] = []; @state() private mapSelect: boolean = false; @state() private fromDateErrorText? = ''; @state() private oldValueLength: number = 0; private debounceEvent: ReturnType | undefined; private debouncedOnFilterValueChange: ( index: number, target: EventTarget ) => void = () => {}; updateActiveFilters() { this.activeFilters = this.filters.filter(filter => filter.value.length > 0); } connectedCallback() { super.connectedCallback(); document.addEventListener('click', this.closeOnOuterClick); window.addEventListener('popstate', this.handlePopState); } disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener('click', this.closeOnOuterClick); window.removeEventListener('popstate', this.handlePopState); } firstUpdated() { this.filterableColumns = this.columns.filter(column => column.filterable); this.filterColumns = this.filterableColumns.map(column => column.name); if (this.readParamsFromURL) { this.filters = this.parseFilterQueryString(); } if (!this.filters.length) { this.addFilter(); } this.updateActiveFilters(); this.dispatchUpdate(false); this.debouncedOnFilterValueChange = (index, target) => { clearTimeout(this.debounceEvent); this.debounceEvent = setTimeout( () => this.onfilterValueChange.bind(this)(index, target), this.filterValueChangeDebounceTime ); }; } get filterNames() { return this.filters.map(filter => filter.columnField); } get unselectedFilters() { return this.filterColumns.filter(f => !this.filterNames.includes(f)); } closeOnOuterClick = (e: Event) => { if (!e.composedPath().includes(this)) { this.isDropdownVisible = false; } }; parseFilterQueryString(): Filter[] { const params = new URLSearchParams(window.location.search); const filters: Filter[] = []; this.filterableColumns.forEach(fc => { fc.filterOperators?.forEach(operator => { const key = `${fc.name}_${operator}`; if (params.has(key)) { filters.push({ columnField: fc.name, operatorValue: operator, dataType: fc.dataType, value: params.get(key) ?? '', }); } }); }); return filters; } dispatchUpdate(resetPage = true) { this.dispatchEvent( new CustomEvent('rowFilter', { detail: { filters: this.filters.filter(filter => filter.value.length), resetPage, }, bubbles: true, composed: true, }) ); } addFilter() { const nextFilter = this.filterColumns.find(filter => !this.filterNames.includes(filter)) || ''; const filterOperators = this.filterableColumns.find( c => c.name === nextFilter )?.filterOperators || ['contains']; const filterDataType = this.filterableColumns.find(c => c.name === nextFilter)?.dataType || 'string'; this.filters = [ ...this.filters, { columnField: nextFilter, operatorValue: filterOperators[0], dataType: filterDataType, value: '', }, ]; this.updateActiveFilters(); } clearFilters() { this.filters = []; this.addFilter(); this.dispatchUpdate(); } removeFilter(index: number) { this.filters = this.filters.filter((_, i) => i !== index); this.dispatchUpdate(); if (this.filters.length === 0) { this.isDropdownVisible = false; this.addFilter(); } this.updateActiveFilters(); } private handlePopState = () => { this.filters = this.parseFilterQueryString(); if (this.filters.length === 0) { this.isDropdownVisible = false; this.addFilter(); } this.updateActiveFilters(); }; private onfilterColumnChange(index: number, e: Event) { const selectedValue = (e.target as HTMLSelectElement).value; const selectedColumn = this.filterableColumns.find( column => column.name === selectedValue ); this.filters[index].columnField = selectedValue; this.filters[index].dataType = selectedColumn?.dataType; this.filters[index].operatorValue = selectedColumn?.filterOperators?.[0] || 'contains'; this.filters = [...this.filters]; this.dispatchUpdate(); } private onfilterOperatorChange(index: number, e: Event) { const selectedValue = (e.target as HTMLSelectElement).value; this.filters[index].operatorValue = selectedValue; this.filters = [...this.filters]; this.dispatchUpdate(); } private onDatefilterValueChange(index: number, value: string) { this.filters[index].value = value; this.dispatchUpdate(); this.updateActiveFilters(); } private onfilterValueChange(index: number, target: EventTarget) { const { value } = target as HTMLInputElement; this.filters[index].value = value; const newValueLength = this.filters[index].value.length; if ( this.filters[index].columnField.length > 0 && (newValueLength >= 3 || newValueLength < this.oldValueLength) ) { this.dispatchUpdate(); } this.updateActiveFilters(); this.oldValueLength = newValueLength; } formatCamelCaseToEnglish(text: string) { const spaced = text.replace(/([A-Z])/g, ' $1').toLowerCase(); const english = spaced.charAt(0).toUpperCase() + spaced.slice(1); return english; } renderToolTip() { if (this.isDropdownVisible) { return copy.hideFilters; } if (!this.activeFilters.length) { return copy.showFilters; } return html`

${this.activeFilters.length} ${copy.activeFilter}

`; } private renderStringInput(value: any, index: number) { return html``; } private renderDateInput(value: any, index: number) { return this.useNewDatePicker ? html` this.onDatefilterValueChange(index, e)} > ` : html` this.onDatefilterValueChange(index, e)} > `; } private renderFilterInputControl(value: any, index: number) { switch (value.dataType) { case 'string': case undefined: return this.renderStringInput(value, index); case 'dateTime': return this.renderDateInput(value, index); default: return nothing; } } renderFilterInput(value: Filter, index: number) { const options = [value.columnField, ...this.unselectedFilters]; const filterOptions = this.filterableColumns.filter(column => options.includes(column.name) ); return html`
this.removeFilter(index)} @keyDown=${(e: KeyboardEvent) => { if (e.key === ' ' || e.key === 'Enter') { this.removeFilter(index); } }} >
${this.mapSelect ? html`
` : nothing}
${this.renderFilterInputControl(value, index)}
`; } renderDropdown() { const disableAddButton = this.filters.length >= this.filterColumns.length || this.activeFilters.length < this.filters.length; return html`
${this.filters.map((filter, index) => this.renderFilterInput(filter, index) )}
`; } render() { return html`
${this.activeFilters.length > 0 ? html`${this.activeFilters.length}` : nothing} { this.isDropdownVisible = !this.isDropdownVisible; }} @keyDown=${() => { this.isDropdownVisible = !this.isDropdownVisible; }} >
filter_list ${copy.filters}
${this.renderToolTip()}
${this.isDropdownVisible ? this.renderDropdown() : nothing}
`; } }