} | null;
if (!cb) continue;
if (cb.updateComplete) await cb.updateComplete;
const labelEl = cb.shadowRoot?.querySelector('[part="label"]') as HTMLElement | null;
const overflowing = !!labelEl && labelEl.scrollWidth > labelEl.clientWidth + 1;
if (overflowing) tip.removeAttribute('disabled');
else tip.setAttribute('disabled', '');
}
}
@watch('selected', { waitUntilFirstUpdate: false })
_onSelectedPropertyChange() {
this._syncSelectedFromProperty();
}
@watch('config', { waitUntilFirstUpdate: false })
_onConfigChange() {
this._applyConfig();
}
private _applyConfig() {
const c = this.config;
if (!c) return;
if (c.variant !== undefined) this.variant = c.variant;
if (c.heading !== undefined) this.heading = c.heading;
if (c.description !== undefined) this.description = c.description;
if (c.itemsLabel !== undefined) this.itemsLabel = c.itemsLabel;
if (c.open !== undefined) this.open = c.open;
if (c.disabled !== undefined) this.disabled = c.disabled;
if (c.items !== undefined) this.items = c.items;
if (c.selected !== undefined) this.selected = c.selected;
if (c.renderItemConfig !== undefined) this.renderItemConfig = c.renderItemConfig;
if (c.allowHtmlLabel !== undefined) this.allowHtmlLabel = c.allowHtmlLabel;
if (c.layout) {
const l = c.layout;
if (l.orientation !== undefined) this.orientation = l.orientation;
if (l.gridRows !== undefined) this.gridRows = l.gridRows;
if (l.gridColumns !== undefined) this.gridColumns = l.gridColumns;
if (l.matrixColumns !== undefined) this.matrixColumns = l.matrixColumns;
if (l.minColumnWidth !== undefined) this.minColumnWidth = l.minColumnWidth;
if (l.laneHeight !== undefined) this.laneHeight = l.laneHeight;
if (l.maxHeight !== undefined) this.maxHeight = l.maxHeight;
if (l.expandIconPlacement !== undefined) this.expandIconPlacement = l.expandIconPlacement;
}
if (c.search) {
if (c.search.placeholder !== undefined) this.searchPlaceholder = c.search.placeholder;
if (c.search.disableLocal !== undefined) this.disableLocalSearch = c.search.disableLocal;
}
if (c.virtualization) {
if (c.virtualization.enabled !== undefined) this.virtualize = c.virtualization.enabled;
if (c.virtualization.threshold !== undefined) this.virtualizeThreshold = c.virtualization.threshold;
if (c.virtualization.overscan !== undefined) this.overscan = c.virtualization.overscan;
}
if (c.pagination) {
if (c.pagination.totalCount !== undefined) this.totalCount = c.pagination.totalCount;
if (c.pagination.pageSize !== undefined) this.pageSize = c.pagination.pageSize;
if (c.pagination.fetchPage !== undefined) this.fetchPage = c.pagination.fetchPage;
if (c.pagination.placeholderLabel !== undefined) this.placeholderLabel = c.pagination.placeholderLabel;
}
if (c.labels) {
if (c.labels.restore !== undefined) this.restoreLabel = c.labels.restore;
if (c.labels.clear !== undefined) this.clearLabel = c.labels.clear;
if (c.labels.selectedOnly !== undefined) this.selectedOnlyLabel = c.labels.selectedOnly;
if (c.labels.showAll !== undefined) this.showAllLabel = c.labels.showAll;
}
if (c.showSelectedToggle !== undefined) this.showSelectedToggle = c.showSelectedToggle;
if (c.preview !== undefined) this.preview = c.preview;
if (c.previewPercentage !== undefined) this.previewPercentage = c.previewPercentage;
}
private _syncSelectedFromProperty() {
this._selectedSet = new Set(this.selected || []);
}
private _getItemValue(item: SelectionItem): string {
const rc = this.renderItemConfig;
if (rc?.getValue) return String(rc.getValue(item));
if (typeof item === 'string') return item;
return String((item as NileDetailSelectionItem).value ?? '');
}
private _getItemLabel(item: SelectionItem): string {
const rc = this.renderItemConfig;
if (rc?.getDisplayText) return rc.getDisplayText(item);
if (typeof item === 'string') return item;
return (item as NileDetailSelectionItem).label ?? this._getItemValue(item);
}
private _getItemSearchText(item: SelectionItem): string {
const rc = this.renderItemConfig;
if (rc?.getSearchText) return rc.getSearchText(item);
return this._getItemLabel(item);
}
private _getItemDescription(item: SelectionItem): string {
const rc = this.renderItemConfig;
if (rc?.getDescription) return rc.getDescription(item) ?? '';
if (typeof item === 'string') return '';
return (item as NileDetailSelectionItem).description ?? '';
}
private _getItemPrefix(item: SelectionItem): string {
const rc = this.renderItemConfig;
if (rc?.getPrefix) return rc.getPrefix(item) ?? '';
if (typeof item === 'string') return '';
return (item as NileDetailSelectionItem).prefix ?? '';
}
private _getItemSuffix(item: SelectionItem): string {
const rc = this.renderItemConfig;
if (rc?.getSuffix) return rc.getSuffix(item) ?? '';
if (typeof item === 'string') return '';
return (item as NileDetailSelectionItem).suffix ?? '';
}
private _getItemDisabled(item: SelectionItem): boolean {
const rc = this.renderItemConfig;
if (rc?.getDisabled) return !!rc.getDisabled(item);
if (typeof item === 'string') return false;
return !!(item as NileDetailSelectionItem).disabled;
}
private _getItemClassName(item: SelectionItem): string {
const rc = this.renderItemConfig;
if (rc?.getClassName) return rc.getClassName(item) ?? '';
if (typeof item === 'string') return '';
return (item as NileDetailSelectionItem).className ?? '';
}
private get _filteredItems(): SelectionItem[] {
let source: SelectionItem[] = this.items;
if (this._showSelectedOnly) {
source = source.filter(item => this._selectedSet.has(this._getItemValue(item)));
}
if (this.disableLocalSearch) return source;
const q = this._searchTerm.trim().toLowerCase();
if (!q) return source;
return source.filter(item =>
this._getItemSearchText(item).toLowerCase().includes(q)
);
}
private _onToggleSelectedOnly(event: Event) {
event.preventDefault();
const next = !this._showSelectedOnly;
if (next && this._selectedSet.size === 0) return;
this._showSelectedOnly = next;
}
private _selectableValues(): string[] {
const source: SelectionItem[] = this._isInfiniteMode
? (Array.from(this._pageCache.values()).flat() as SelectionItem[])
: this.items;
return source
.filter(i => !this._getItemDisabled(i))
.map(i => this._getItemValue(i));
}
private get _allChecked(): boolean {
const selectable = this._selectableValues();
if (selectable.length === 0) return false;
return selectable.every(v => this._selectedSet.has(v));
}
private get _indeterminate(): boolean {
const selectable = this._selectableValues();
const sel = selectable.filter(v => this._selectedSet.has(v)).length;
return sel > 0 && sel < selectable.length;
}
private get _countLabel(): string {
const total = this._isInfiniteMode ? (this.totalCount ?? 0) : this.items.length;
return `${this._selectedSet.size} of ${total} ${this.itemsLabel} selected`;
}
private _emitSelectionChange() {
this.selected = [...this._selectedSet];
this.dispatchEvent(
new CustomEvent('nile-change', {
detail: { selected: [...this._selectedSet] },
bubbles: true,
composed: true,
})
);
}
private _onItemChange(value: string, disabled: boolean, event: Event) {
if (disabled) {
event.preventDefault();
return;
}
const target = event.target as HTMLInputElement;
const next = new Set(this._selectedSet);
if (target.checked) next.add(value);
else next.delete(value);
this._selectedSet = next;
this._emitSelectionChange();
}
private _onSelectAllChange(event: Event) {
const target = event.target as HTMLInputElement;
const selectable = this._selectableValues();
const next = new Set(this._selectedSet);
if (target.checked) selectable.forEach(v => next.add(v));
else selectable.forEach(v => next.delete(v));
this._selectedSet = next;
this._emitSelectionChange();
}
private _emitSearch(value: string) {
this.dispatchEvent(
new CustomEvent('nile-search', {
detail: { value },
bubbles: true,
composed: true,
})
);
}
private _onSearchInput(event: Event) {
const target = event.target as HTMLInputElement;
const value = target.value ?? '';
this._searchTerm = value;
if (this.disableLocalSearch) this._emitSearch(value);
}
private _onSearchClear() {
this._searchTerm = '';
if (this.disableLocalSearch) this._emitSearch('');
}
private _onRestore(event: Event) {
event.preventDefault();
const source = this._restoreDefaults ?? this._selectableValues();
this._selectedSet = new Set(source);
this._emitSelectionChange();
}
private _onClear(event: Event) {
event.preventDefault();
this._selectedSet = new Set();
this._showSelectedOnly = false;
this._emitSelectionChange();
}
private _handleDefaultSlotChange(event: Event) {
const slot = event.target as HTMLSlotElement;
const nodes = slot.assignedNodes({ flatten: true });
this._hasSlottedContent = nodes.some(node => {
if (node.nodeType === Node.ELEMENT_NODE) return true;
if (node.nodeType === Node.TEXT_NODE) return !!(node.textContent && node.textContent.trim());
return false;
});
if (this._isPreviewActive) {
this._applyPreviewHeight();
}
}
private _stopHeaderToggle(event: Event) {
event.stopPropagation();
}
private _handleSummaryClick(event: Event) {
event.preventDefault();
handleSummaryClick(this);
}
private _handleSummaryKeyDown(event: KeyboardEvent) {
handleSummaryKeyDown(event, this);
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open) {
this._detailOpen = true;
await animateShow(this);
} else {
await animateHide(this);
// In preview mode the details element stays open so the preview remains visible.
this._detailOpen = this._previewEnabled;
}
}
@watch(['preview', 'previewPercentage'], { waitUntilFirstUpdate: true })
handlePreviewChange() {
if (this.open) {
return;
}
if (this._previewEnabled) {
this._detailOpen = true;
this._applyPreviewHeight();
} else {
this.body.hidden = true;
this.body.style.height = '0';
this._detailOpen = false;
}
}
/** Measures the content and clamps the body to the configured preview height. */
private async _applyPreviewHeight() {
await this.updateComplete;
if (!this._isPreviewActive || !this.body) {
return;
}
this.body.hidden = false;
this.body.style.height = `${getPreviewHeight(this)}px`;
}
private _handlePreviewClick() {
if (this._isPreviewActive && !this.disabled) {
showDetail(this);
}
}
private _handlePreviewKeyDown(event: KeyboardEvent) {
if (!this._isPreviewActive || this.disabled) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
showDetail(this);
}
}
async show() {
return showDetail(this);
}
async hide() {
return hideDetail(this);
}
private get _summaryLabel(): string {
return [this.heading, this.description].filter(Boolean).join(', ');
}
private _renderSelectionLabel(): TemplateResult {
return html`
${this.heading}
${this._countLabel}
`;
}
private _renderSelectionBody(): TemplateResult {
const virtualized = this._isVirtualized;
const isVertical = this.orientation === 'vertical';
const isBoth = this.orientation === 'both';
const laneHeightPx = this.laneHeight;
const columnWidth = this._columnWidthPx;
let gridStyle = '';
if (virtualized) {
if (isBoth) {
gridStyle = `max-height: ${this.maxHeight}; height: ${this.maxHeight};`;
} else if (isVertical) {
gridStyle = `max-height: ${this.maxHeight}; height: ${this.maxHeight};`;
} else {
const lanes = Math.max(1, this.gridRows);
gridStyle = `height: ${lanes * laneHeightPx}px;`;
}
} else if (isVertical) {
const cols = Math.max(1, this.gridColumns);
gridStyle = `grid-template-columns: repeat(${cols}, minmax(0, 1fr)); grid-auto-flow: row; max-height: ${this.maxHeight}; overflow-y: auto; overflow-x: hidden;`;
} else {
const lanes = Math.max(1, this.gridRows);
gridStyle = `grid-template-rows: repeat(${lanes}, auto); grid-auto-columns: minmax(${this.minColumnWidth}, 1fr);`;
}
return html`
${this.showSelectedToggle
? html`
${this._showSelectedOnly ? this.showAllLabel : this.selectedOnlyLabel}
`
: nothing}
${this.restoreLabel}
${this.clearLabel}
${virtualized
? (isBoth
? this._renderBothVirtualGrid(laneHeightPx, columnWidth)
: this._renderVirtualGrid(laneHeightPx, columnWidth, isVertical))
: this._renderStaticGrid()}
`;
}
private _renderItemContent(item: SelectionItem): TemplateResult {
const label = this._getItemLabel(item);
const description = this._getItemDescription(item);
const prefix = this._getItemPrefix(item);
const suffix = this._getItemSuffix(item);
const allowHtml = this.allowHtmlLabel;
return html`
${prefix
? html`${allowHtml ? unsafeHTML(prefix) : prefix}`
: ''}
${allowHtml ? unsafeHTML(label) : label}
${description
? html`${allowHtml ? unsafeHTML(description) : description}`
: ''}
${suffix
? html`${allowHtml ? unsafeHTML(suffix) : suffix}`
: ''}
`;
}
private _renderItemTooltip(item: SelectionItem, opts: { positionStyle?: string; extraClasses?: string } = {}): TemplateResult {
const value = this._getItemValue(item);
const label = this._getItemLabel(item);
const description = this._getItemDescription(item);
const isDisabled = this._getItemDisabled(item);
const className = this._getItemClassName(item);
const tooltipContent = description ? `${label} — ${description}` : label;
const tooltipClass = `detail__selection-tooltip${opts.extraClasses ? ' ' + opts.extraClasses : ''}`;
const checkboxClass = classMap({
'detail__selection-checkbox': true,
'detail__selection-checkbox--disabled': isDisabled,
[className]: !!className,
});
return html`
this._onItemChange(value, isDisabled, e)}
>${this._renderItemContent(item)}
`;
}
private _renderStaticGrid(): TemplateResult {
return html`${this._filteredItems.map(item => this._renderItemTooltip(item))}`;
}
private _renderBothVirtualGrid(_laneHeightPx: number, _columnWidthPx: number): TemplateResult {
const rowV = this._rowVirtCtrl?.getVirtualizer();
const colV = this._colVirtCtrl?.getVirtualizer();
const rowTotal = rowV?.getTotalSize() ?? 0;
const colTotal = colV?.getTotalSize() ?? 0;
const rowItems = rowV?.getVirtualItems() ?? [];
const colItems = colV?.getVirtualItems() ?? [];
const cols = Math.max(1, this.matrixColumns);
const total = this._filteredItems.length;
return html`
${rowItems.flatMap(rvi =>
colItems.map(cvi => {
const idx = rvi.index * cols + cvi.index;
if (idx >= total) return null;
const item = this._filteredItems[idx];
if (item === undefined) return null;
const positionStyle = `position: absolute; top: ${rvi.start}px; left: ${cvi.start}px; width: ${cvi.size}px; height: ${rvi.size}px;`;
return this._renderItemTooltip(item, {
positionStyle,
extraClasses: 'detail__selection-tooltip--virtual',
});
})
)}
`;
}
private _renderVirtualGrid(
laneHeightPx: number,
_columnWidthPx: number,
isVertical: boolean
): TemplateResult {
const v = this._virtCtrl?.getVirtualizer();
const totalSize = v?.getTotalSize() ?? 0;
const virtualItems = v?.getVirtualItems() ?? [];
const lanes = Math.max(1, isVertical ? this.gridColumns : this.gridRows);
if (this._isInfiniteMode && virtualItems.length > 0) {
const first = virtualItems[0].index;
const last = virtualItems[virtualItems.length - 1].index;
this._scheduleFetchForRange(first, last);
}
const trackStyle = isVertical
? `width: 100%; height: ${totalSize}px; position: relative;`
: `width: ${totalSize}px; height: 100%; position: relative;`;
return html`
${virtualItems.map(vi => {
const laneIdx = vi.lane ?? 0;
const positionStyle = isVertical
? `position: absolute; top: ${vi.start}px; left: calc(${laneIdx} * (100% / ${lanes})); width: calc(100% / ${lanes}); height: ${vi.size}px;`
: `position: absolute; top: ${laneIdx * laneHeightPx}px; left: ${vi.start}px; width: ${vi.size}px; height: ${laneHeightPx}px;`;
const item = this._getItemAt(vi.index);
if (item === undefined) {
return this._renderPlaceholder(positionStyle);
}
return this._renderItemTooltip(item, {
positionStyle,
extraClasses: 'detail__selection-tooltip--virtual',
});
})}
`;
}
private _renderPlaceholder(positionStyle: string): TemplateResult {
return html`
`;
}
render(): TemplateResult {
const isSelection = this._isSelectionVariant;
return html`
${isSelection
? html`
${this._renderSelectionBody()}
`
: ''}
`;
}
}
export default NileDetail;
declare global {
interface HTMLElementTagNameMap {
'nile-detail': NileDetail;
}
}