import { type Placement, type MiddlewareData, type ComputePositionConfig, type Boundary } from '@floating-ui/dom'; export class PortalUtils { static calculateAvailableSpace(referenceElement: HTMLElement): { spaceAbove: number; spaceBelow: number; viewportHeight: number; } { const rect = referenceElement.getBoundingClientRect(); const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - rect.bottom; const spaceAbove = rect.top; return { spaceAbove, spaceBelow, viewportHeight }; } static getOptimalPlacement(referenceElement: HTMLElement): Placement { const { spaceAbove, spaceBelow } = this.calculateAvailableSpace(referenceElement); if (spaceBelow < 200 && spaceAbove > spaceBelow) { return 'top'; } return 'bottom'; } static findBoundaryElements(component: HTMLElement): Element[] | undefined { const boundaryElements: Element[] = []; let currentElement = component.parentElement; while (currentElement && currentElement !== document.body) { const computedStyle = window.getComputedStyle(currentElement); const overflow = computedStyle.overflow; const overflowY = computedStyle.overflowY; const overflowX = computedStyle.overflowX; if (overflow === 'auto' || overflow === 'scroll' || overflowY === 'auto' || overflowY === 'scroll' || overflowX === 'auto' || overflowX === 'scroll') { boundaryElements.push(currentElement); } if (currentElement.hasAttribute('data-floating-boundary') || currentElement.classList.contains('floating-boundary') || currentElement.classList.contains('scroll-container')) { boundaryElements.push(currentElement); } currentElement = currentElement.parentElement; } return boundaryElements.length > 0 ? boundaryElements : undefined; } static calculateOptimalHeight( referenceRect: { x: number; y: number; width: number; height: number }, viewportHeight: number, placement: Placement ): number { const spaceBelow = viewportHeight - (referenceRect.y + referenceRect.height); const spaceAbove = referenceRect.y; if (spaceAbove > spaceBelow) { return Math.max(spaceAbove - 20, 100); } else if (spaceBelow > spaceAbove) { return Math.max(spaceBelow - 20, 100); } return Math.max(Math.min(spaceAbove, spaceBelow) - 20, 100); } static extractStylesAsCSS(styles: any): string { if (typeof styles === 'string') { return styles; } if (Array.isArray(styles)) { return styles.map(style => this.extractStylesAsCSS(style)).join('\n'); } if (styles && typeof styles === 'object' && styles.cssText) { return styles.cssText; } return ''; } static generateStyleId(): string { return `nile-select-styles-${Math.random().toString(36).substring(2, 11)}`; } static isPositioningOptimal( placement: Placement, referenceElement: HTMLElement ): boolean { const { spaceAbove, spaceBelow } = this.calculateAvailableSpace(referenceElement); const isAbove = placement.startsWith('top'); const isBelow = placement.startsWith('bottom'); if (isAbove && spaceBelow > spaceAbove) return false; if (isBelow && spaceAbove > spaceBelow) return false; return true; } static applyCollisionData( element: HTMLElement, middlewareData: MiddlewareData, placement: Placement ): void { if (middlewareData.flip) { const { overflows } = middlewareData.flip; element.setAttribute('data-placement', placement); if (overflows && overflows.length > 0) { const overflowPlacements = overflows.map(overflow => overflow.placement).join(','); element.setAttribute('data-overflow', overflowPlacements); } else { element.removeAttribute('data-overflow'); } } if (middlewareData.shift) { const { x, y } = middlewareData.shift; if (x !== undefined && y !== undefined && (x !== 0 || y !== 0)) { element.setAttribute('data-shift', `${x},${y}`); } else { element.removeAttribute('data-shift'); } } if (middlewareData.size) { const { availableWidth, availableHeight } = middlewareData.size; if (availableWidth !== undefined) { element.setAttribute('data-available-width', availableWidth.toString()); } if (availableHeight !== undefined) { element.setAttribute('data-available-height', availableHeight.toString()); } } } static createFloatingUIMiddleware( boundary: Boundary, onSizeApply: (params: { availableWidth: number; availableHeight: number; elements: { floating: HTMLElement }; rects: { reference: { x: number; y: number; width: number; height: number } }; }) => void ): ComputePositionConfig['middleware'] { return []; } } export class PortalContentUtils { static createBaseListbox(component: any): HTMLElement { const listbox = document.createElement('div'); listbox.className = `select__listbox ${component.searchEnabled ? 'select__search-enabled ' : ''}`; listbox.setAttribute('tabindex', '-1'); return listbox; } static addSearchSection(listbox: HTMLElement, component: any): void { if (!component.searchEnabled) return; const searchDiv = document.createElement('div'); searchDiv.className = 'select__search'; searchDiv.innerHTML = ` `; listbox.appendChild(searchDiv); } static addLoadingSection(listbox: HTMLElement, component: any): void { if (!component.optionsLoading) return; const loaderSpan = document.createElement('span'); loaderSpan.className = 'select__loader'; loaderSpan.innerHTML = ` `; listbox.appendChild(loaderSpan); } static addOptionsSection(listbox: HTMLElement, component: any): void { const optionsContainer = document.createElement('div'); optionsContainer.className = `select__options ${component.searchEnabled ? 'select__options__search-enabled' : ''}`; this.addNoResultsMessage(optionsContainer, component); this.addClonedOptions(optionsContainer, component); listbox.appendChild(optionsContainer); } static addNoResultsMessage(container: HTMLElement, component: any): void { if (!component.showNoResults) return; const noResultsDiv = document.createElement('div'); noResultsDiv.className = 'select__no-results'; noResultsDiv.textContent = component.noResultsMessage; container.appendChild(noResultsDiv); } static addClonedOptions(container: HTMLElement, component: any): void { const options = Array.from(component.querySelectorAll('nile-option')); options.forEach((option: Element) => { const clonedOption = this.cloneOption(option as HTMLElement); container.appendChild(clonedOption); }); } static cloneOption(option: HTMLElement): HTMLElement { const clonedOption = option.cloneNode(true) as HTMLElement; Array.from(option.attributes).forEach((attr: Attr) => { clonedOption.setAttribute(attr.name, attr.value); }); this.copyOptionProperties(option, clonedOption); return clonedOption; } static copyOptionProperties(originalOption: HTMLElement, clonedOption: HTMLElement): void { const properties = ['selected', 'disabled', 'current', 'hidden']; properties.forEach(prop => { if ((originalOption as any)[prop] !== undefined) { (clonedOption as any)[prop] = (originalOption as any)[prop]; } }); } static addFooterSection(listbox: HTMLElement, component: any): void { const hasCustomFooter = component.querySelectorAll('[slot="pre-footer"]').length > 0; const hasBuiltInFooter = component.multiple; if (!hasCustomFooter && !hasBuiltInFooter) return; const footerArea = document.createElement('div'); footerArea.setAttribute('part', 'footer-area'); footerArea.className = 'select__footer-area'; if (hasCustomFooter) { this.addCustomFooter(footerArea, component); } if (hasBuiltInFooter) { const builtInFooter = this.createFooter(component); footerArea.appendChild(builtInFooter); } listbox.appendChild(footerArea); } static addCustomFooter(container: HTMLElement, component: any): void { const footerContent = Array.from(component.querySelectorAll('[slot="pre-footer"]')); if (footerContent.length === 0) return; const customFooter = document.createElement('div'); customFooter.className = 'select__custom-footer'; customFooter.setAttribute('part', 'custom-footer'); footerContent.forEach((el: Element) => { customFooter.appendChild(el.cloneNode(true)); }); customFooter.addEventListener('click', () => component.hide()); container.appendChild(customFooter); } static createFooter(component: any): HTMLElement { const footerDiv = document.createElement('div'); footerDiv.setAttribute('part', 'footer'); footerDiv.className = 'select__footer'; this.addShowSelectedToggle(footerDiv, component); this.addClearAllButton(footerDiv, component); return footerDiv; } static addShowSelectedToggle(footer: HTMLElement, component: any): void { const showSelectedSpan = document.createElement('span'); showSelectedSpan.style.cursor = 'pointer'; const checkbox = document.createElement('nile-checkbox'); checkbox.disabled = component.selectedOptions.length === 0; checkbox.checked = component.showSelected; checkbox.innerHTML = '  Show Selected'; showSelectedSpan.appendChild(checkbox); footer.appendChild(showSelectedSpan); } static addClearAllButton(footer: HTMLElement, component: any): void { if (component.selectedOptions.length === 0) return; const clearAllSpan = document.createElement('span'); clearAllSpan.className = 'select__clear'; clearAllSpan.textContent = 'Clear All'; clearAllSpan.style.cursor = 'pointer'; footer.appendChild(clearAllSpan); } static updateClonedOptions(clonedListbox: HTMLElement, component: any): void { const optionsContainer = clonedListbox.querySelector('.select__options') as HTMLElement; if (!optionsContainer) return; const originalOptions = Array.from(component.querySelectorAll('nile-option')); const clonedOptions = Array.from(optionsContainer.querySelectorAll('nile-option')); originalOptions.forEach((originalOption: Element, index: number) => { const clonedOption = clonedOptions[index] as HTMLElement; if (clonedOption) { this.updateClonedOption(originalOption as HTMLElement, clonedOption); } }); } static updateClonedOption(originalOption: HTMLElement, clonedOption: HTMLElement): void { (clonedOption as any).selected = (originalOption as any).selected; (clonedOption as any).disabled = (originalOption as any).disabled; (clonedOption as any).current = (originalOption as any).current; (clonedOption as any).hidden = (originalOption as any).hidden; this.updateClonedOptionAttributes(originalOption, clonedOption); } static updateClonedOptionAttributes(originalOption: HTMLElement, clonedOption: HTMLElement): void { const attributes = [ { prop: 'selected', attr: 'selected' }, { prop: 'disabled', attr: 'disabled' }, { prop: 'current', attr: 'current' }, { prop: 'hidden', attr: 'hidden' } ]; attributes.forEach(({ prop, attr }) => { if ((originalOption as any)[prop]) { clonedOption.setAttribute(attr, ''); } else { clonedOption.removeAttribute(attr); } }); } static createPortalListbox(component: any): HTMLElement { const listbox = this.createBaseListbox(component); this.addSearchSection(listbox, component); this.addLoadingSection(listbox, component); this.addOptionsSection(listbox, component); this.addFooterSection(listbox, component); return listbox; } } export class PortalEventUtils { static setupPortalEventListeners(clonedListbox: HTMLElement, component: any): void { if (!clonedListbox) return; this.setupOptionClickListeners(clonedListbox, component); this.setupScrollListeners(clonedListbox, component); this.setupSearchListeners(clonedListbox, component); this.setupFooterListeners(clonedListbox, component); this.setupSlotListeners(clonedListbox, component); } static setupOptionClickListeners(clonedListbox: HTMLElement, component: any): void { if (!clonedListbox) return; clonedListbox.addEventListener('mouseup', (event) => { const target = event.target as HTMLElement; const option = target.closest('nile-option'); if (option) { this.handleOptionClick(option as HTMLElement, component); } }); } static handleOptionClick(option: HTMLElement, component: any): void { const originalOption = component.querySelector(`nile-option[value="${option.getAttribute('value')}"]`) as HTMLElement; if (originalOption) { const syntheticEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }); Object.defineProperty(syntheticEvent, 'target', { value: originalOption, writable: false }); if (component.handleOptionClick) { component.handleOptionClick(syntheticEvent); } } } static setupScrollListeners(clonedListbox: HTMLElement, component: any): void { if (!clonedListbox) return; clonedListbox.addEventListener('scroll', (event) => { if (component.handleScroll) { component.handleScroll(event); } }); } static setupSearchListeners(clonedListbox: HTMLElement, component: any): void { if (!clonedListbox) return; const searchInput = clonedListbox.querySelector('nile-input') as HTMLElement; if (searchInput) { searchInput.addEventListener('nile-input', (event) => { if (component.handleSearchChange) { component.handleSearchChange(event); } }); searchInput.addEventListener('nile-focus', (event) => { if (component.handleSearchFocus) { component.handleSearchFocus(); } }); searchInput.addEventListener('nile-blur', (event) => { if (component.handleSearchBlur) { component.handleSearchBlur(); } }); } } static setupFooterListeners(clonedListbox: HTMLElement, component: any): void { if (!clonedListbox) return; const footer = clonedListbox.querySelector('.select__footer') as HTMLElement; if (footer) { this.setupFooterClickHandlers(footer); this.setupShowSelectedToggle(footer, component); this.setupClearAllHandler(footer, component); } this.setupCustomFooterListeners(clonedListbox, component); } static setupCustomFooterListeners(clonedListbox: HTMLElement, component: any): void { const customFooter = clonedListbox.querySelector('.select__custom-footer') as HTMLElement; if (!customFooter) return; customFooter.addEventListener('click', (event) => { event.stopPropagation(); }); } static setupFooterClickHandlers(footer: HTMLElement): void { footer.addEventListener('click', (event) => { event.stopPropagation(); event.preventDefault(); }); } static setupShowSelectedToggle(footer: HTMLElement, component: any): void { const showSelectedSpan = footer.querySelector('span[style*="cursor: pointer"]'); if (showSelectedSpan) { showSelectedSpan.addEventListener('click', (event) => { event.stopPropagation(); event.preventDefault(); if (component.toggleShowSelected) { component.toggleShowSelected(event); } }); } } static setupClearAllHandler(footer: HTMLElement, component: any): void { footer.addEventListener('click', (event) => { const target = event.target as HTMLElement; if (target.classList.contains('select__clear')) { event.stopPropagation(); event.preventDefault(); if (component.unSlectAll) { component.unSlectAll(); } } }); } static setupSlotListeners(clonedListbox: HTMLElement, component: any): void { if (!clonedListbox) return; const slot = clonedListbox.querySelector('slot'); if (slot) { slot.addEventListener('slotchange', () => { if (component.updatePortalContent) { component.updatePortalContent(); } }); } } }