/** * Copyright Aquera Inc 2025 * * 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 { html, type TemplateResult } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { classMap } from 'lit/directives/class-map.js'; import { repeat } from 'lit/directives/repeat.js'; import type { VirtualItem } from '@tanstack/virtual-core'; import type { ComboboxRow, ComboboxHeaderRow } from './types'; export class ComboboxRenderer { static renderGroupHeader(row: ComboboxHeaderRow): TemplateResult { return html` `; } static renderRowsPlain( rows: ComboboxRow[], value: string | string[], multiple: boolean, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, showNoResults: boolean, noResultsMessage: string, isLoading: boolean, onScroll: (e: Event) => void, allowHtmlLabel: boolean, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, enableDescription?: boolean, noResultsSubtitle?: string, ): TemplateResult { if (showNoResults && !isLoading && rows.length === 0) { return ComboboxRenderer.renderNoResults(noResultsMessage, noResultsSubtitle); } return html`
${rows.map((row) => row.kind === 'header' ? ComboboxRenderer.renderGroupHeader(row) : ComboboxRenderer.renderItem( row.item, value, multiple, getDisplayText, getItemValue, allowHtmlLabel, getItemDescription, getItemPrefix, getItemSuffix, enableDescription, ), )}
`; } static renderRowsVirtualized( virtualItems: VirtualItem[], totalSize: number, rows: ComboboxRow[], value: string | string[], multiple: boolean, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, isLoading: boolean, allowHtmlLabel: boolean, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, enableDescription?: boolean, ): TemplateResult { return html`
${repeat( virtualItems, (vItem) => vItem.key, (vItem) => { const row = rows[vItem.index]; if (!row) return html``; const posStyle = `position:absolute;top:0;left:0;right:0;` + `transform:translateY(${vItem.start}px);` + `height:${vItem.size}px;`; if (row.kind === 'header') { return html`
${ComboboxRenderer.renderGroupHeader(row)}
`; } return html`
${ComboboxRenderer.renderItem( row.item, value, multiple, getDisplayText, getItemValue, allowHtmlLabel, getItemDescription, getItemPrefix, getItemSuffix, enableDescription, )}
`; }, )}
`; } static renderVirtualizedOptions( virtualItems: VirtualItem[], totalSize: number, data: any[], value: string | string[], multiple: boolean, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, isLoading: boolean, allowHtmlLabel: boolean, measureElement: (el: Element | null) => void, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, enableDescription?: boolean, ): TemplateResult { const offsetTop = virtualItems.length > 0 ? virtualItems[0].start : 0; return html`
${repeat( virtualItems, (vItem) => vItem.key, (vItem) => { const item = data[vItem.index]; return ComboboxRenderer.renderMeasuredItem( item, vItem.index, value, multiple, getDisplayText, getItemValue, allowHtmlLabel, getItemDescription, getItemPrefix, getItemSuffix, enableDescription, ); }, )}
`; } static renderPlainOptions( data: any[], value: string | string[], multiple: boolean, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, showNoResults: boolean, noResultsMessage: string, isLoading: boolean, onScroll: (e: Event) => void, allowHtmlLabel: boolean, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, enableDescription?: boolean, noResultsSubtitle?: string, ): TemplateResult { if (showNoResults && !isLoading && data.length === 0) { return ComboboxRenderer.renderNoResults(noResultsMessage, noResultsSubtitle); } return html`
${data.map((item: any) => ComboboxRenderer.renderItem( item, value, multiple, getDisplayText, getItemValue, allowHtmlLabel, getItemDescription, getItemPrefix, getItemSuffix, enableDescription, ), )}
`; } static renderNoResults(noResultsMessage: string, noResultsSubtitle?: string): TemplateResult { return html`
${noResultsMessage || 'No result found'}
${noResultsSubtitle ? html`
${noResultsSubtitle}
` : ''}
`; } static renderNoData(noDataMessage: string): TemplateResult { return html`
${noDataMessage || 'No data available'}
`; } private static renderMeasuredItem( item: any, index: number, value: string | string[], multiple: boolean, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, allowHtmlLabel: boolean, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, enableDescription?: boolean, ): TemplateResult { if (!item) return html``; const optionValue = getItemValue(item); const displayText = getDisplayText(item); const isDisabled = item?.disabled || false; const className = item?.className; const description = getItemDescription ? getItemDescription(item) : (item?.description ?? ''); const prefix = getItemPrefix ? getItemPrefix(item) : (item?.prefix ?? ''); const suffix = getItemSuffix ? getItemSuffix(item) : (item?.suffix ?? ''); let isSelected = false; if (multiple) { isSelected = Array.isArray(value) && value.some(v => String(v) === String(optionValue)); } else { isSelected = String(Array.isArray(value) ? value[0] : value) === String(optionValue); } return html` ${unsafeHTML(prefix)} ${allowHtmlLabel ? unsafeHTML(displayText) : displayText} ${unsafeHTML(suffix)} `; } static renderItem( item: any, value: string | string[], multiple: boolean, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, allowHtmlLabel: boolean, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, enableDescription?: boolean, ): TemplateResult { if (!item) return html``; const optionValue = getItemValue(item); const displayText = getDisplayText(item); const isDisabled = item?.disabled || false; const className = item?.className; const description = getItemDescription ? getItemDescription(item) : (item?.description ?? ''); const prefix = getItemPrefix ? getItemPrefix(item) : (item?.prefix ?? ''); const suffix = getItemSuffix ? getItemSuffix(item) : (item?.suffix ?? ''); let isSelected = false; if (multiple) { isSelected = Array.isArray(value) && value.some(v => String(v) === String(optionValue)); } else { isSelected = String(Array.isArray(value) ? value[0] : value) === String(optionValue); } return html` ${unsafeHTML(prefix)} ${allowHtmlLabel ? unsafeHTML(displayText) : displayText} ${unsafeHTML(suffix)} `; } static renderVirtualizedGrid( virtualItems: VirtualItem[], totalSize: number, data: any[], value: string | string[], multiple: boolean, gridColumns: number, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, isLoading: boolean, allowHtmlLabel: boolean, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, gridColumnWidth?: number, ): TemplateResult { const offsetTop = virtualItems.length > 0 ? virtualItems[0].start : 0; const colTemplate = gridColumnWidth ? `repeat(${gridColumns}, ${gridColumnWidth}px)` : `repeat(${gridColumns}, 1fr)`; return html`
${repeat( virtualItems, (vItem) => vItem.key, (vItem) => { const rowStart = vItem.index * gridColumns; const rowItems = data.slice(rowStart, rowStart + gridColumns); return html`
${rowItems.map((item: any) => ComboboxRenderer.renderItem( item, value, multiple, getDisplayText, getItemValue, allowHtmlLabel, getItemDescription, getItemPrefix, getItemSuffix, ), )}
`; }, )}
`; } static renderHorizontalGrid( virtualItems: VirtualItem[], totalSize: number, data: any[], value: string | string[], multiple: boolean, gridRows: number, gridColumnWidth: number, getDisplayText: (item: any) => string, getItemValue: (item: any) => string, isLoading: boolean, allowHtmlLabel: boolean, getItemDescription?: (item: any) => string, getItemPrefix?: (item: any) => string, getItemSuffix?: (item: any) => string, ): TemplateResult { const offsetLeft = virtualItems.length > 0 ? virtualItems[0].start : 0; const rowHeight = 38; return html`
${repeat( virtualItems, (vCol) => vCol.key, (vCol) => { const colStart = vCol.index * gridRows; const colItems = data.slice(colStart, colStart + gridRows); return html`
${colItems.map((item: any) => ComboboxRenderer.renderItem( item, value, multiple, getDisplayText, getItemValue, allowHtmlLabel, getItemDescription, getItemPrefix, getItemSuffix, ), )}
`; }, )}
`; } static renderAddCustomOption( searchValue: string, multiple: boolean, ): TemplateResult { return html` + Add "${searchValue}" `; } static shouldUseVirtualizer(data: any[], gridColumns = 1): boolean { if (gridColumns > 1) return true; return data.length >= 5; } }