import { html, css, LitElement, PropertyValues, nothing } from 'lit' import { repeat } from 'lit/directives/repeat.js' import { ifDefined } from 'lit/directives/if-defined.js' import { property, state, customElement } from 'lit/decorators.js' import { InputData, Values } from './definition-schema.js' import '@vaadin/grid' import '@vaadin/grid/vaadin-grid-sort-column.js' import { columnBodyRenderer } from '@vaadin/grid/lit.js' type Column = Exclude[number] type RowData = { [key: string]: Values[number] } type Theme = { theme_name: string theme_object: any } @customElement('widget-table-versionplaceholder') export class WidgetTable extends LitElement { @property({ type: Object }) inputData?: InputData @property({ type: Object }) theme?: Theme @state() rows: RowData[] = [] @state() private themeBgColor?: string @state() private themeTitleColor?: string @state() private themeSubtitleColor?: string @state() private themeRowHoverColor?: string @state() private themeBorderColor?: string version: string = 'versionplaceholder' update(changedProperties: Map) { if (changedProperties.has('inputData')) { this.transformInputData() } 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 cssHoverColor = getComputedStyle(this).getPropertyValue('--re-hover-color').trim() const cssBorderColor = getComputedStyle(this).getPropertyValue('--re-border-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 this.themeRowHoverColor = cssHoverColor || this.computeHoverColor(this.themeBgColor) this.themeBorderColor = cssBorderColor || this.computeBorderColor(this.themeTitleColor) } private computeHoverColor(bgColor?: string): string { // Create a subtle hover effect based on background if (!bgColor || bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') { return 'rgba(128, 128, 128, 0.1)' } return 'rgba(128, 128, 128, 0.1)' } private computeBorderColor(textColor?: string): string { // Create a subtle border color based on text color if (!textColor) { return 'rgba(128, 128, 128, 0.2)' } return 'rgba(128, 128, 128, 0.2)' } transformInputData() { if (!this?.inputData?.columns?.length) { this.rows = [] return } const cols = this.inputData.columns.map((col) => col?.values ?? []) const maxLength = Math.max(...cols.map((vals) => vals?.length ?? 0)) const rows: RowData[] = [] for (let r = 0; r < maxLength; r++) { const row: RowData = {} for (let c = 0; c < cols.length; c++) { row[`col_${c}`] = cols?.[c]?.[r] ?? {} } rows.push(row) } this.rows = rows.reverse() } renderCell(cell: Values[number], colIndex: number) { const colDef = this?.inputData?.columns?.[colIndex] if (!colDef) return nothing switch (colDef?.type) { case 'string': return this.renderString(cell?.value, colDef) case 'number': return this.renderNumber(cell?.value, colDef) case 'boolean': return this.renderBoolean(cell?.value, colDef) case 'state': return this.renderState(cell?.value, colDef) case 'button': return this.renderButton(cell, colDef) case 'image': return this.renderImage(cell, colDef) case 'timestamp': return this.renderTimestamp(cell?.value, colDef) default: return html`${cell?.value ?? ''}` } } renderString(value: string | undefined, colDef: Column) { return html`${value ?? ''}` } renderNumber(value: any, colDef: Column) { const num = typeof value === 'number' ? value : parseFloat(value) if (isNaN(num)) return '' const precision = colDef?.styling?.precision ?? 0 return html`${num.toFixed(precision)}` } renderBoolean(value: any, colDef: Column) { return value ? '✓' : '-' } renderState(value: any, colDef: Column) { const _stateMap = colDef.styling?.stateMap ?.split(',') .map((d: string) => d.trim().replaceAll("'", '')) const stateMap = _stateMap?.reduce((p: any, c: string, i: number, a: any[]) => { if (i % 2 === 0) p[c] = a[i + 1] return p }, {}) return html`
` } renderButton(cell: Values[number], colDef: Column) { return html`${cell?.value ?? ''}` } renderImage(cell: Values[number], colDef: Column) { if (!cell?.value) return nothing return html`` } renderTimestamp(value: any, colDef: Column) { if (value === undefined || value === null || value === '') return '' const parseFormat = colDef.styling?.timestampParseFormat const displayFormat = colDef.styling?.timestampFormat let date: Date if (parseFormat) { // Parse using custom format date = this.parseTimestamp(String(value), parseFormat) } else { // Default: expect Unix epoch milliseconds const timestamp = typeof value === 'number' ? value : parseInt(value, 10) if (isNaN(timestamp)) return '' date = new Date(timestamp) } if (isNaN(date.getTime())) return '' if (displayFormat) { return html`${this.formatTimestamp(date, displayFormat)}` } return html`${date.toISOString()}` } parseTimestamp(value: string, format: string): Date { // Unicode LDML format tokens: yyyy, MM, dd, HH, mm, ss, SSS // See: https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table const formatTokens: { [key: string]: { regex: string; setter: (d: Date, v: number) => void } } = { yyyy: { regex: '(\\d{4})', setter: (d, v) => d.setFullYear(v) }, MM: { regex: '(\\d{2})', setter: (d, v) => d.setMonth(v - 1) }, dd: { regex: '(\\d{2})', setter: (d, v) => d.setDate(v) }, HH: { regex: '(\\d{2})', setter: (d, v) => d.setHours(v) }, mm: { regex: '(\\d{2})', setter: (d, v) => d.setMinutes(v) }, ss: { regex: '(\\d{2})', setter: (d, v) => d.setSeconds(v) }, SSS: { regex: '(\\d{3})', setter: (d, v) => d.setMilliseconds(v) } } let regexStr = format.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') const tokens: string[] = [] for (const token of ['yyyy', 'SSS', 'MM', 'dd', 'HH', 'mm', 'ss']) { if (regexStr.includes(token)) { regexStr = regexStr.replace(token, formatTokens[token].regex) tokens.push(token) } } const match = value.match(new RegExp(`^${regexStr}$`)) if (!match) return new Date(NaN) const date = new Date(0) tokens.forEach((token, i) => { formatTokens[token].setter(date, parseInt(match[i + 1], 10)) }) return date } formatTimestamp(date: Date, format: string): string { const pad = (n: number, len = 2) => String(n).padStart(len, '0') // Unicode LDML format tokens // See: https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table return format .replace('yyyy', String(date.getFullYear())) .replace('MM', pad(date.getMonth() + 1)) .replace('dd', pad(date.getDate())) .replace('HH', pad(date.getHours())) .replace('mm', pad(date.getMinutes())) .replace('ss', pad(date.getSeconds())) .replace('SSS', pad(date.getMilliseconds(), 3)) } getTextAlign(colDef: Column) { switch (colDef.type) { case 'number': case 'timestamp': return 'end' case 'button': case 'string': return 'start' case 'boolean': case 'state': case 'image': return 'center' default: return 'start' } } static styles = css` :host { display: flex; flex-direction: column; font-family: sans-serif; box-sizing: border-box; position: relative; height: 100%; width: 100%; flex: 1; } .paging:not([active]) { display: none !important; } .wrapper { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; } h3 { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 16px 0px 0px 16px; box-sizing: border-box; } p { margin: 10px 0 16px 0; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 16px; box-sizing: border-box; } vaadin-grid { flex: 1; --vaadin-grid-cell-padding: var(--cell-padding-vertical, 8px) var(--cell-padding-horizontal, 16px); --vaadin-grid-column-border-width: var(--grid-column-border-width, 0); --vaadin-grid-border-color: var(--grid-border-color, rgba(128, 128, 128, 0.3)); background: var(--grid-bg-color, transparent); } vaadin-grid::part(cell) { background: var(--grid-bg-color, transparent); color: var(--grid-text-color, inherit); } vaadin-grid::part(header-cell) { font-size: var(--header-font-size, 14px); background: var(--grid-header-bg-color, transparent); color: var(--grid-text-color, inherit); } vaadin-grid::part(body-cell) { background: var(--grid-bg-color, transparent); color: var(--grid-text-color, inherit); } vaadin-grid::part(even-row-cell) { background: var(--grid-bg-color, transparent); } vaadin-grid::part(odd-row-cell) { background: var(--grid-bg-color, transparent); } vaadin-grid-sorter { flex: 1; justify-content: inherit; } vaadin-grid-cell-content { display: flex; align-items: center; height: 100%; } .grid-container { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; overflow: hidden; } .grid-container.overflow { overflow-x: auto; } .grid-container.overflow vaadin-grid { min-width: max-content; } .statusbox { width: 24px; height: 12px; border-radius: 6px; } img { max-width: 100%; max-height: 40px; object-fit: contain; } .no-data { font-size: 20px; display: flex; height: 100%; width: 100%; text-align: center; align-items: center; justify-content: center; } .cell { width: 100%; height: 100%; display: flex; align-items: center; } .cell-center { justify-content: center; } .cell-end { justify-content: flex-end; } .cell-start { justify-content: flex-start; } a, a:visited, a:hover, a:active { color: inherit; text-decoration: underline; } ` render() { const columns = this.inputData?.columns ?? [] return html`

${this.inputData?.title}

${this.inputData?.subTitle}

${repeat( columns, (col, i) => `${col.header}-${i}`, (col, colIndex) => html` ( (item) => html`
${this.renderCell(item[`col_${colIndex}`], colIndex)}
`, [colIndex, col, this.themeTitleColor] )} >
` )}
No Data
` } }