import { Directive, Input, ElementRef, Renderer2, AfterViewInit, } from '@angular/core'; // interfaces import { IItemsDropdownList } from '../components/ca-items-dropdown/interfaces'; @Directive({ selector: '[appDescriptionItemsTextCount]', standalone: true, }) export class DescriptionItemsTextCountDirective implements AfterViewInit { @Input('appDescriptionItemsTextCount') set items(value: T[]) { this._items = value; this.createStringArray(this._items); this.updateItems(); } @Input() itemsSpecialStylesIndexArray?: number[] = []; @Input() containerWidth: number = 0; @Input() separator: string = '•'; private _items: T[] | string[] = []; constructor( private element: ElementRef, private renderer: Renderer2 ) {} ngAfterViewInit(): void { this.updateItems(); } private createStringArray(value: T[]): void { const isStringArray = value?.every((item) => typeof item === 'string'); if (!isStringArray && Array.isArray(value)) { const objectArray = value as { code?: string; description?: string; nickname?: string; itemFuel?: { name?: string; }; }[]; this._items = objectArray.map( (item) => item.code || item.description || item.nickname || item.itemFuel?.name || '' ); } } private updateItems(): void { const container = this.element.nativeElement; // clear the container and set styles this.renderer.setProperty(container, 'innerHTML', ''); this.renderer.setStyle(container, 'display', 'flex'); this.renderer.setStyle(container, 'overflow', 'hidden'); this.renderer.setStyle(container, 'white-space', 'nowrap'); this.renderer.setStyle(container, 'align-items', 'center'); if (!this._items?.length) return; // Calculate available width for items const totalItems = this._items.length; let fittedItems: Array<{ element: HTMLElement; width: number }> = []; let currentWidth = 0; let overflowCount = 0; // First pass: try to fit all items without reserving overflow space for (let i = 0; i < totalItems; i++) { const item = this._items[i]; const isLastItem = i === totalItems - 1; // Calculate widths const itemWidth = this.getTextWidth(item); const separatorWidth = isLastItem ? 0 : this.getTextWidth(` ${this.separator} `) + 8; // 8px for margin // Check if current item fits const totalWidthNeeded = currentWidth + itemWidth + separatorWidth; if (totalWidthNeeded <= this.containerWidth) { // Item fits, add it const itemSpan = this.createSpan(item, i); fittedItems.push({ element: itemSpan, width: itemWidth }); currentWidth += itemWidth; // Add separator if not the last item if (!isLastItem) { const separatorSpan = this.createSpan( ` ${this.separator} ` ); this.renderer.setStyle( separatorSpan, 'margin', '0 4px 0 4px' ); this.renderer.setStyle( separatorSpan, 'user-select', 'none' ); fittedItems.push({ element: separatorSpan, width: separatorWidth, }); currentWidth += separatorWidth; } } else { // Items don't fit, we need to handle overflow const remainingItems = totalItems - i; const estimatedOverflowWidth = this.getTextWidth(`+${remainingItems}`) + 16; // padding for margin // Check if we can fit some items by removing the last separator and making room for overflow if (fittedItems.length > 0 && !isLastItem) { // Remove last separator if it exists const lastItem = fittedItems[fittedItems.length - 1]; if ( lastItem.element.textContent?.includes(this.separator) ) { fittedItems.pop(); currentWidth -= lastItem.width; } } // Check if we have room for overflow indicator if ( currentWidth + estimatedOverflowWidth <= this.containerWidth ) { overflowCount = remainingItems; } else { // Remove items until we have space for overflow indicator while ( fittedItems.length > 0 && currentWidth + estimatedOverflowWidth > this.containerWidth ) { const removedItem = fittedItems.pop()!; currentWidth -= removedItem.width; overflowCount++; } // Recalculate overflow count text width in case the number changed const actualOverflowWidth = this.getTextWidth(`+${overflowCount}`) + 16; if ( currentWidth + actualOverflowWidth > this.containerWidth && fittedItems.length > 0 ) { const removedItem = fittedItems.pop()!; currentWidth -= removedItem.width; overflowCount++; } } break; } } // Append fitted items to container fittedItems.forEach((item) => { this.renderer.appendChild(container, item.element); }); // Add overflow count if necessary if (overflowCount > 0) { const overflowContainer = this.renderer.createElement('div'); this.renderer.addClass(overflowContainer, 'items-counter-count'); this.renderer.setStyle(overflowContainer, 'margin-left', '8px'); this.renderer.setStyle(overflowContainer, 'flex-shrink', '0'); const overflowSpan = this.createSpan( `+${overflowCount}`, null, true ); this.renderer.setStyle(overflowSpan, 'user-select', 'none'); this.renderer.appendChild(overflowContainer, overflowSpan); this.renderer.appendChild(container, overflowContainer); } } private createSpan( text: T | string, index?: number | null, isOverflowCountSpan?: boolean ): HTMLElement { const span = this.renderer.createElement('span'); this.renderer.setProperty(span, 'textContent', text); this.renderer.addClass(span, 'text-color-black-2'); this.renderer.addClass( span, isOverflowCountSpan ? 'text-size-11' : 'text-size-14' ); this.renderer.addClass( span, isOverflowCountSpan ? 'ca-font-medium' : 'ca-font-regular' ); this.renderer.setStyle(span, 'white-space', 'nowrap'); // apply special style if the index is in itemSpecialStylesIndexArray if (this.itemsSpecialStylesIndexArray?.includes(index as number)) this.renderer.addClass(span, 'text-color-blue-18'); return span; } private getTextWidth(text: T | string): number { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d')!; const style = getComputedStyle(this.element.nativeElement); context.font = `${style.fontSize} ${style.fontFamily}`; return context.measureText(text as string).width; } }