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}
`
}
}