/**
* 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`
`;
}
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;
}
}