import { html, css, LitElement, PropertyValues } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { NavigationButtonConfiguration } from './definition-schema' import '@material/web/icon/icon.js' import '@material/web/button/filled-button.js' type Theme = { theme_name: string theme_object: any } /** * ECharts themes describe a *chart canvas*, and most of them set * `backgroundColor` to a fully transparent colour (the light theme uses * `rgba(0, 0, 0, 0)`). That is a valid, truthy colour string, so using it * verbatim as the button background paints nothing at all. Treat any * fully transparent theme colour as "no colour" so the fallback kicks in. */ const isTransparent = (color?: string) => { if (!color) return true const value = color.trim().toLowerCase() if (value === 'transparent') return true const functional = value.match(/^(?:rgba|hsla)\([^)]*[,/]\s*([\d.]+%?)\s*\)$/) if (functional) return parseFloat(functional[1]) === 0 // #rrggbbaa / #rgba if (/^#[0-9a-f]{8}$/.test(value)) return value.slice(7) === '00' if (/^#[0-9a-f]{4}$/.test(value)) return value[4] === '0' return false } @customElement('widget-navbutton-versionplaceholder') export class WidgetNavbutton extends LitElement { @property({ type: Object }) inputData?: NavigationButtonConfiguration @property({ type: Object }) theme?: Theme @property({ type: String }) route?: string @state() private themeBgColor?: string @state() private themeTitleColor?: string version: string = 'versionplaceholder' update(changedProperties: Map) { if (changedProperties.has('theme')) { this.registerTheme(this.theme) } super.update(changedProperties) } protected firstUpdated(_changedProperties: PropertyValues): void { this.registerTheme(this.theme) } registerTheme(theme?: Theme) { const cssTextColor = getComputedStyle(this).getPropertyValue('--re-text-color').trim() const cssBgColor = getComputedStyle(this).getPropertyValue('--re-tile-background-color').trim() const themeBgColor = theme?.theme_object?.backgroundColor this.themeBgColor = cssBgColor || (isTransparent(themeBgColor) ? undefined : themeBgColor) this.themeTitleColor = cssTextColor || theme?.theme_object?.title?.textStyle?.color } handleNavItemClick() { let goto: string if (this.inputData?.route === '') { goto = '/' + this.route?.split('/').filter(Boolean).slice(0, -1).join('/') + '/' || '/' goto = goto === '//' ? '/' : goto } else { goto = this.resolveRoute({ route: this.inputData?.route, variables: this.inputData?.variables }) || '/' } const event = new CustomEvent('nav-submit', { detail: { path: goto }, bubbles: true, composed: true }) this.dispatchEvent(event) } resolveRoute(item?: any): string | undefined { let route = String(item?.route ?? '') if (item?.variables) { for (const variable of item.variables) { if (variable.label) { route = route .split(`{{${variable.label}}}`) .join(encodeURIComponent(String(variable.value ?? ''))) } } } if (route.includes('*')) { const currentSegments = (this.route || '').split('/').filter(Boolean) const routeSegments = route.split('/').filter(Boolean) for (let i = 0; i < routeSegments.length; i++) { if (routeSegments[i] === '*') { routeSegments[i] = currentSegments[i] ?? '' } } route = (route.startsWith('/') ? '/' : '') + routeSegments.filter(Boolean).join('/') } return route } static styles = css` :host { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-family: sans-serif; box-sizing: border-box; margin: auto; padding: 2cqh 2cqw; } md-icon { font-family: 'Material Symbols Outlined'; } md-filled-button { flex: 1; --md-filled-button-container-shape: 4px; --md-filled-button-container-shadow-color: none; /* State layers follow the configured label color; a hard-coded accent stays teal no matter how the button is styled. */ --md-ripple-hover-color: var(--btn-fg); --md-ripple-pressed-color: var(--btn-fg); --md-filled-button-hover-state-layer-color: var(--btn-fg); --md-filled-button-pressed-state-layer-color: var(--btn-fg); } ` render() { const fontSize = this.inputData?.style?.fontSize ?? 16 const iconFontSize = fontSize * 1.5 const fontColor = this.inputData?.style?.color || this.themeTitleColor || 'black' const bgColor = this.inputData?.style?.backgroundColor || this.themeBgColor || 'white' // The container height is a fixed 40px token, so it has to grow with the // label or larger font sizes overflow the button. const buttonHeight = Math.max(40, Math.round(fontSize * 2.5)) return html` this.handleNavItemClick()} > ${this.inputData?.title} ${this.inputData?.iconName} ` } }