import { html, css, LitElement, PropertyValueMap, nothing } from 'lit' import { repeat } from 'lit/directives/repeat.js' import { customElement, property, query, state } from 'lit/decorators.js' import { InputData } from './definition-schema.js' type Theme = { theme_name: string theme_object: any } @customElement('widget-embed-versionplaceholder') export class WidgetValue extends LitElement { @property({ type: Object }) inputData?: InputData @property({ type: Object }) theme?: Theme @state() private themeBgColor?: string @state() private themeTitleColor?: string @state() private themeSubtitleColor?: string @state() private showFailMessage: boolean = false @state() private gridColumns: number = 1 @state() private elementWidth: number = 0 @state() private elementHeight: number = 0 @query('.wrapper') private wrapper?: HTMLDivElement version: string = 'versionplaceholder' private resizeObserver?: ResizeObserver disconnectedCallback() { super.disconnectedCallback() if (this.resizeObserver) { this.resizeObserver.disconnect() } } protected firstUpdated(_changedProperties: PropertyValueMap | Map) { this.registerTheme(this.theme) // Set up ResizeObserver for the wrapper if (this.wrapper) { this.resizeObserver = new ResizeObserver(() => { this.updateGridLayout() }) this.resizeObserver.observe(this.wrapper) // Initial layout calculation this.updateGridLayout() } } update(changedProperties: Map) { if (changedProperties.has('inputData')) { this.updateGridLayout() } if (changedProperties.has('theme')) { this.registerTheme(this.theme) } super.update(changedProperties) } registerTheme(theme?: Theme) { const cssTextColor = getComputedStyle(this).getPropertyValue('--re-text-color').trim() const cssBgColor = getComputedStyle(this).getPropertyValue('--re-tile-background-color').trim() this.themeBgColor = cssBgColor || this.theme?.theme_object?.backgroundColor this.themeTitleColor = cssTextColor || this.theme?.theme_object?.title?.textStyle?.color this.themeSubtitleColor = cssTextColor || this.theme?.theme_object?.title?.subtextStyle?.color || this.themeTitleColor } /** * Update grid layout based on container size and number of elements */ updateGridLayout() { if (!this.wrapper) return const urls = this.inputData?.multiEmbed ? (this.inputData?.data?.map((item) => item.webUrl) ?? []) : [this.inputData?.webUrl ?? ''] const numElements = urls.length if (numElements === 0) return const rect = this.wrapper.getBoundingClientRect() const containerWidth = rect.width const containerHeight = rect.height if (containerWidth === 0 || containerHeight === 0) return // Maximize total area (negative because optimizeLayout minimizes error) const layout = this.optimizeLayout( containerWidth, containerHeight, numElements, (w, h) => Math.abs(w / h - 16 / 10), this.inputData?.gap ?? 12 ) // Update grid columns and element dimensions this.elementWidth = layout.elementWidth this.elementHeight = layout.elementHeight console.log('Optimized layout:', layout) // Apply width and height to each child iframe const iframes = this.wrapper.querySelectorAll('.iframe-container') as NodeListOf iframes.forEach((iframe) => { iframe.style.width = `${layout.elementWidth}px` iframe.style.height = `${layout.elementHeight}px` }) } /** * Optimize grid layout by minimizing an error function * @param containerWidth - Available width for the grid * @param containerHeight - Available height for the grid * @param numElements - Number of elements to arrange * @param errorFn - Error function that takes (element_width, element_height) and returns error value * @param gap - Gap between elements (default: 12) * @returns Optimal layout configuration */ optimizeLayout( containerWidth: number, containerHeight: number, numElements: number, errorFn: (w: number, h: number) => number, gap: number = 12 ): { elementWidth: number elementHeight: number numCols: number numRows: number error: number } { let bestLayout = { elementWidth: 0, elementHeight: 0, numCols: 1, numRows: numElements, error: Infinity } // Try all possible column configurations from 1 to numElements for (let cols = 1; cols <= numElements; cols++) { const rows = Math.ceil(numElements / cols) // Calculate element dimensions accounting for gaps const totalGapWidth = gap * (cols - 1) const totalGapHeight = gap * (rows - 1) const availableWidth = containerWidth - totalGapWidth const availableHeight = containerHeight - totalGapHeight if (availableWidth <= 0 || availableHeight <= 0) continue const elementWidth = availableWidth / cols const elementHeight = availableHeight / rows // Calculate error for this configuration const error = errorFn(elementWidth, elementHeight) // Update best layout if this is better if (error < bestLayout.error) { bestLayout = { elementWidth, elementHeight, numCols: cols, numRows: rows, error } } } return bestLayout } static styles = css` :host { display: block; font-family: sans-serif; box-sizing: border-box; position: relative; margin: auto; } .paging:not([active]) { display: none !important; } .wrapper { display: flex; flex-wrap: wrap; height: 100%; width: 100%; box-sizing: border-box; gap: 12px; align-content: flex-start; } .wrapper > * { flex-shrink: 1; min-width: 0; min-height: 0; } .title { height: 24px; font-size: 18px; margin: 0; padding: 0 4px; overflow: hidden; text-overflow: ellipsis; text-align: center; flex: 0 0 auto; white-space: nowrap; } .no-data { font-size: 20px; display: flex; flex-direction: column; height: 100%; width: 100%; text-align: center; align-items: center; justify-content: center; } .iframe-container { display: flex; flex-direction: column; box-sizing: border-box; max-width: 100%; max-height: 100%; overflow: hidden; } iframe { border: none; flex: 1; min-width: 0; min-height: 0; width: 100%; height: 100%; } ` render() { const urls = this.inputData?.multiEmbed ? (this.inputData?.data ?? []) : [{ url: this.inputData?.webUrl ?? '', title: this.inputData?.title ?? '' }] const hasMultipleItems = urls.length > 1 const showTitles = urls.filter((u) => u.title).length > 0 return html` Unable to display the content. The site may have restrictions preventing it from being embedded. ${this.inputData?.webUrl} ${repeat( urls, (url) => url.url, (url) => html` ${showTitles ? html`${url.title ?? ''}` : nothing} ` )} ` } }