import { type Placement, type MiddlewareData } from '@floating-ui/dom'; import { render } from 'lit-html'; 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-auto-complete-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()); } } } } export class PortalContentUtils { static createBaseMenu(component: any): HTMLElement { const menu = document.createElement('nile-menu'); menu.id = 'content-menu'; menu.className = component.enableVirtualScroll ? 'virtualized__menu' : ''; if (component.enableVirtualScroll) { menu.setAttribute('exportparts', 'menu__items-wrapper:options__wrapper'); } else { menu.setAttribute('exportparts', 'menu__items-wrapper:options__wrapper'); } return menu; } static addMenuItems(menu: HTMLElement, component: any): void { const menuItems = component.menuItems || []; menuItems.forEach((item: any) => { const menuItem = this.createMenuItem(item, component); menu.appendChild(menuItem); }); } static createMenuItem(item: any, component: any): HTMLElement { const container = document.createElement('div'); const templateResult = component.getItemRenderFunction(item); render(templateResult, container); return container.firstElementChild as HTMLElement; } static createPortalMenu(component: any): HTMLElement { const menu = this.createBaseMenu(component); this.addMenuItems(menu, component); return menu; } static updatePortalMenuItems(clonedMenu: HTMLElement, component: any): void { if (!clonedMenu) return; // Remove existing items const existingItems = clonedMenu.querySelectorAll('nile-menu-item'); existingItems.forEach(item => item.remove()); // Add updated items this.addMenuItems(clonedMenu, component); } } export class PortalEventUtils { static setupPortalEventListeners(clonedMenu: HTMLElement, component: any): void { if (!clonedMenu) return; this.setupMenuSelectListeners(clonedMenu, component); } static setupMenuSelectListeners(clonedMenu: HTMLElement, component: any): void { if (!clonedMenu) return; clonedMenu.addEventListener('nile-select', (event: Event) => { const customEvent = event as CustomEvent; if (component.handleSelect) { // Call the component's handleSelect method directly component.handleSelect(customEvent); } }); } }