diff --git a/rollup.config.js b/rollup.config.js index bd5201b..3e26bad 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -36,11 +36,37 @@ const plugins = [ export default [ { - input: 'src/energy-period-selector-plus.ts', + input: ['src/energy-period-selector-plus.ts'], output: { dir: 'dist', format: 'es', + inlineDynamicImports: true, + }, + plugins: [ + minifyHTML(), + terser({ output: { comments: false } }), + typescript({ + declaration: false, + }), + nodeResolve(), + json({ + compact: true, + }), + commonjs(), + babel({ + exclude: "node_modules/**", + babelHelpers: "bundled", + }), + ...(dev ? [serve(serveOptions)] : [terser()]), + ], + moduleContext: (id) => { + const thisAsWindowForModules = [ + "node_modules/@formatjs/intl-utils/lib/src/diff.js", + "node_modules/@formatjs/intl-utils/lib/src/resolve-locale.js", + ]; + if (thisAsWindowForModules.some((id_) => id.trimRight().endsWith(id_))) { + return "window"; + } }, - plugins: [...plugins], }, ]; diff --git a/src/energy-period-selector-plus-base.ts b/src/energy-period-selector-plus-base.ts index 09d61fd..d96b6da 100644 --- a/src/energy-period-selector-plus-base.ts +++ b/src/energy-period-selector-plus-base.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { mdiCompare, mdiCompareRemove } from '@mdi/js'; +import { mdiCompare, mdiCompareRemove, mdiCalendarToday } from '@mdi/js'; import { addDays, addMonths, @@ -17,7 +17,6 @@ import { startOfToday, startOfWeek, startOfYear, - format, } from 'date-fns/esm'; import { UnsubscribeFunc } from 'home-assistant-js-websocket'; import { html, LitElement, nothing } from 'lit'; @@ -77,7 +76,7 @@ export class EnergyPeriodSelectorBase extends SubscribeMixin(LitElement) { return this.hass.localize(`ui.panel.lovelace.components.energy_period_selector.${period}`) || localize(`toggleButtons.${period}`); }; - const viewButtons: ToggleButton[] = !this._config?.period_buttons + const periodButtons: ToggleButton[] = !this._config?.period_buttons ? [ { label: this.hass.localize('ui.panel.lovelace.components.energy_period_selector.day'), @@ -127,6 +126,22 @@ export class EnergyPeriodSelectorBase extends SubscribeMixin(LitElement) { `; + + const todayButtonText = html` + ${this.hass.localize('ui.panel.lovelace.components.energy_period_selector.today')} + `; + + const todayButtonIcon = html` + `; + + const todayButton = + this._config?.today_button_type === false ? nothing : this._config?.today_button_type === 'icon' ? todayButtonIcon : todayButtonText; + return html`
${this._period === 'custom' @@ -152,22 +167,18 @@ export class EnergyPeriodSelectorBase extends SubscribeMixin(LitElement) { > ` : nothing} - ${this._config?.today_button !== false - ? html` - ${this.hass.localize('ui.panel.lovelace.components.energy_period_selector.today')} - ` - : nothing} + ${todayButton}
`}
- ${this._config?.compare_button === 'icon' + ${this._config?.compare_button_type === 'icon' ? html` - ${this.hass.localize('ui.panel.lovelace.components.energy_period_selector.compare')} + ${this._config.compare_button_label ?? this.hass.localize('ui.panel.lovelace.components.energy_period_selector.compare')} ` - : this._config?.compare_button === 'text' + : this._config?.compare_button_type === 'text' ? html` - ${this.hass.localize('ui.panel.lovelace.components.energy_period_selector.compare')} + ${this._config.compare_button_label ?? this.hass.localize('ui.panel.lovelace.components.energy_period_selector.compare')} ` : nothing}
diff --git a/src/energy-period-selector-plus-config.ts b/src/energy-period-selector-plus-config.ts index b1afe61..3106983 100644 --- a/src/energy-period-selector-plus-config.ts +++ b/src/energy-period-selector-plus-config.ts @@ -3,9 +3,10 @@ import { EnergyCardBaseConfig } from './type/energy-card-base-config'; export interface EnergyPeriodSelectorPlusConfig extends LovelaceCardConfig, EnergyCardBaseConfig { card_background?: boolean; - today_button?: boolean; prev_next_buttons?: boolean; - compare_button?: string; + compare_button_type?: string; + compare_button_label?: string; + today_button_type?: string | boolean; period_buttons?: string[]; custom_period_label?: string; } diff --git a/src/energy-period-selector-plus.ts b/src/energy-period-selector-plus.ts index 84d9d8a..4b7e665 100644 --- a/src/energy-period-selector-plus.ts +++ b/src/energy-period-selector-plus.ts @@ -9,6 +9,7 @@ import './energy-period-selector-plus-base'; import { localize } from './localize/localize'; import { logError } from './logging'; import { styles } from './style'; +import { LovelaceCardEditor } from 'custom-card-helpers'; registerCustomCard({ type: 'energy-period-selector-plus', @@ -25,13 +26,18 @@ export class EnergyPeriodSelectorPlus extends LitElement implements LovelaceCard return 1; } + public static async getConfigElement(): Promise { + await import('./ui-editor/ui-editor'); + return document.createElement('energy-period-selector-editor'); + } + public setConfig(config: EnergyPeriodSelectorPlusConfig): void { this._config = config; } protected render() { if (!this.hass || !this._config) { - logError(localize('common.invalid_configuration')); + logError(localize('common.invalid_configuration') || 'Invalid configuration'); return nothing; } const EnergyPeriodSelectorBase = html` diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 484c5aa..3bc341b 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -20,33 +20,17 @@ "entity_editor": "Entity editor", "decimals": "decimals", "fields": { - "autoconfig": "Autoconfig", - "print_yaml": "Print auto generated config yaml", - "show_names": "Show names", - "show_icons": "Show icons", - "show_states": "Show states", - "show_units": "Show units", - "energy_date_selection": "Sync with energy_date_selection component", - "height": "Height", - "wide": "Wide", - "min_box_height": "Min box height", - "min_box_distance": "Min box distance", - "min_state": "Min state", - "round": "Round", - "throttle": "Throttle", - "unit_prefix": "Unit prefix", - "entity": "Entity", - "type": "Type", - "children": "Children", - "name": "Name", - "icon": "Icon", - "color": "Color", - "unit_of_measurement": "Unit of measurement", - "tap_action": "Tap action", - "color_on_state": "Change color based on state", - "color_limit": "State limit for color change", - "color_above": "Color above limit", - "color_below": "Color below limit" + "card_background": "Card Background", + "prev_next_buttons": "Show Previous/Next Buttons", + "compare_button_type": "Compare Button Type", + "custom_period_label": "Custom Period Label", + "compare_button_options": { + "icon": "Icon", + "text": "Text" + }, + "period_buttons": "Period Buttons", + "today_button_type": "Today Button Type", + "compare_button_label": "Compare Button Label" }, "entity_types": { "entity": "Entity", diff --git a/src/localize/localize.ts b/src/localize/localize.ts index c6c1660..2b70399 100644 --- a/src/localize/localize.ts +++ b/src/localize/localize.ts @@ -9,10 +9,10 @@ const languages: any = { 'pt-PT': pt_PT, }; -export function localize(string: string, search = '', replace = ''): string { +export function localize(string: string, search = '', replace = '') { const lang = (localStorage.getItem('selectedLanguage') || 'en').replace(/['"]+/g, '').replace('-', '_'); - let translated: string; + let translated: string | undefined; try { translated = string.split('.').reduce((o, i) => o[i], languages[lang]); @@ -23,7 +23,7 @@ export function localize(string: string, search = '', replace = ''): string { if (translated === undefined) translated = string.split('.').reduce((o, i) => o && o[i], languages['en']); if (search !== '' && replace !== '') { - translated = translated.replace(search, replace); + translated = translated?.replace(search, replace); } - return translated || string; + return translated; } diff --git a/src/style.ts b/src/style.ts index 91767ad..05182cf 100644 --- a/src/style.ts +++ b/src/style.ts @@ -48,7 +48,7 @@ export const stylesBase = css` mwc-button { margin-left: 8px; } - ha-icon-button { + ha-icon-button:not(.today-icon) { margin-left: 4px; --mdc-icon-size: 20px; } @@ -81,7 +81,7 @@ export const stylesBase = css` --mdc-button-disabled-ink-color: var(--disabled-text-color); --mdc-icon-button-ripple-opacity: 0.2; } - ha-icon-button { + ha-icon-button:not(.today-icon) { --mdc-icon-button-size: 28px; } ha-button-toggle-group { diff --git a/src/ui-editor/types/schema-union.ts b/src/ui-editor/types/schema-union.ts new file mode 100644 index 0000000..a64a96e --- /dev/null +++ b/src/ui-editor/types/schema-union.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { LitElement } from "lit"; + + + +export type HaFormSchema = + | HaFormConstantSchema + | HaFormStringSchema + | HaFormIntegerSchema + | HaFormFloatSchema + | HaFormBooleanSchema + | HaFormSelectSchema + | HaFormMultiSelectSchema + | HaFormTimeSchema + | HaFormSelector + | HaFormGridSchema + | HaFormExpandableSchema; + +export interface HaFormBaseSchema { + name: string; + // This value is applied if no data is submitted for this field + default?: HaFormData; + required?: boolean; + disabled?: boolean; + description?: { + suffix?: string; + // This value will be set initially when form is loaded + suggested_value?: HaFormData; + }; + context?: Record; +} + +export interface HaFormGridSchema extends HaFormBaseSchema { + type: "grid"; + name: string; + column_min_width?: string; + schema: readonly HaFormSchema[]; +} + +export interface HaFormExpandableSchema extends HaFormBaseSchema { + type: "expandable"; + name: ""; + title: string; + icon?: string; + iconPath?: string; + expanded?: boolean; + headingLevel?: 1 | 2 | 3 | 4 | 5 | 6; + schema: readonly HaFormSchema[]; +} + +export interface HaFormSelector extends HaFormBaseSchema { + type?: never; + selector: any; +} + +export interface HaFormConstantSchema extends HaFormBaseSchema { + type: "constant"; + value?: string; +} + +export interface HaFormIntegerSchema extends HaFormBaseSchema { + type: "integer"; + default?: HaFormIntegerData; + valueMin?: number; + valueMax?: number; +} + +export interface HaFormSelectSchema extends HaFormBaseSchema { + type: "select"; + options: ReadonlyArray; +} + +export interface HaFormMultiSelectSchema extends HaFormBaseSchema { + type: "multi_select"; + options: + | Record + | readonly string[] + | ReadonlyArray; +} + +export interface HaFormFloatSchema extends HaFormBaseSchema { + type: "float"; +} + +export interface HaFormStringSchema extends HaFormBaseSchema { + type: "string"; + format?: string; + autocomplete?: string; +} + +export interface HaFormBooleanSchema extends HaFormBaseSchema { + type: "boolean"; +} + +export interface HaFormTimeSchema extends HaFormBaseSchema { + type: "positive_time_period_dict"; +} + +// Type utility to unionize a schema array by flattening any grid schemas +export type SchemaUnion< + SchemaArray extends readonly HaFormSchema[], + Schema = SchemaArray[number] +> = Schema extends HaFormGridSchema | HaFormExpandableSchema + ? SchemaUnion + : Schema; + +export interface HaFormDataContainer { + [key: string]: HaFormData; +} + +export type HaFormData = + | HaFormStringData + | HaFormIntegerData + | HaFormFloatData + | HaFormBooleanData + | HaFormSelectData + | HaFormMultiSelectData + | HaFormTimeData; + +export type HaFormStringData = string; +export type HaFormIntegerData = number; +export type HaFormFloatData = number; +export type HaFormBooleanData = boolean; +export type HaFormSelectData = string; +export type HaFormMultiSelectData = string[]; +export type HaFormTimeData = any; + +export interface HaFormElement extends LitElement { + schema: HaFormSchema | readonly HaFormSchema[]; + data?: HaFormDataContainer | HaFormData; + label?: string; +} diff --git a/src/ui-editor/ui-editor.ts b/src/ui-editor/ui-editor.ts new file mode 100644 index 0000000..896b0df --- /dev/null +++ b/src/ui-editor/ui-editor.ts @@ -0,0 +1,224 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-use-before-define */ + +import { LitElement, css, html, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { fireEvent, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers'; +import { EnergyCardBaseConfig } from '../type/energy-card-base-config'; +import { any, assert, assign, boolean, integer, number, object, optional, string } from 'superstruct'; +import { localize } from '../localize/localize'; +import memoizeOne from 'memoize-one'; +import { EnergyPeriodSelectorPlusConfig } from '../energy-period-selector-plus-config'; +import { SchemaUnion } from './types/schema-union'; + +export const loadHaForm = async () => { + if (customElements.get('ha-form')) return; + + const helpers = await (window as any).loadCardHelpers?.(); + if (!helpers) return; + const card = await helpers.createCardElement({ type: 'entity' }); + if (!card) return; + await card.getConfigElement(); +}; + +@customElement('energy-period-selector-editor') +export class EnergyPeriodSelectorEditor extends LitElement implements LovelaceCardEditor { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config?: EnergyPeriodSelectorPlusConfig; + + public async setConfig(config: EnergyPeriodSelectorPlusConfig): Promise { + assert( + config, + assign( + object({ + type: string(), + view_layout: optional(string()), + }), + object({ + card_background: optional(boolean()), + today_button: optional(boolean()), + prev_next_buttons: optional(boolean()), + compare_button_type: optional(string()), + today_button_type: optional(any()), + period_buttons: optional(any()), + custom_period_label: optional(string()), + compare_button_label: optional(string()), + }), + ), + ); + this._config = config; + } + + connectedCallback(): void { + super.connectedCallback(); + loadHaForm(); + } + + private _schema = memoizeOne( + (showCompareLabel, showCustomPeriodLabel) => + [ + { + type: 'grid', + name: '', + schema: [ + { + name: 'card_background', + selector: { boolean: {} }, + }, + { + name: 'prev_next_buttons', + selector: { boolean: {} }, + }, + ], + }, + { + type: 'grid', + name: '', + schema: [ + { + name: 'compare_button_type', + selector: { + select: { + options: [ + { value: '', label: '' }, + { value: 'icon', label: localize('editor.fields.compare_button_options.icon') }, + { value: 'text', label: localize('editor.fields.compare_button_options.text') }, + ], + mode: 'dropdown', + }, + }, + }, + ...(showCompareLabel + ? ([ + { + name: 'compare_button_label', + selector: { text: {} }, + }, + ] as const) + : []), + ], + }, + { + name: 'today_button_type', + selector: { + select: { + options: [ + { value: false, label: '' }, + { value: 'icon', label: localize('editor.fields.compare_button_options.icon') }, + { value: 'text', label: localize('editor.fields.compare_button_options.text') }, + ], + mode: 'dropdown', + }, + }, + }, + { + type: 'grid', + name: '', + schema: [ + { + type: 'multi_select', + options: { + day: 'day', + week: 'week', + month: 'month', + year: 'year', + custom: 'custom', + }, + name: 'period_buttons', + default: ['day', 'week', 'month', 'year'], + }, + ...(showCustomPeriodLabel + ? ([ + { + name: 'custom_period_label', + selector: { text: {} }, + }, + ] as const) + : []), + ], + }, + ] as const, + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + const data = { + ...this._config, + card_background: this._config.card_background ?? false, + today_button: this._config.today_button ?? true, + prev_next_buttons: this._config.prev_next_buttons ?? true, + compare_button_type: this._config.compare_button_type ?? '', + today_button_type: this._config.today_button_type ?? 'text', + period_buttons: this._config.period_buttons ?? ['day', 'week', 'month', 'year'], + custom_period_label: this._config.custom_period_label ?? localize(`toggleButtons.custom`), + compare_button_label: this._config.compare_button_label ?? this.hass.localize('ui.panel.lovelace.components.energy_period_selector.compare'), + }; + + const schema = this._schema(data.compare_button_type === 'text', data.period_buttons.includes('custom')); + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + const config = ev?.detail.value; + fireEvent(this, 'config-changed', { config }); + } + + private _computeLabelCallback = schema => { + return localize(`editor.fields.${schema.name}`) || `not found: ${schema.name}`; + }; + + static get styles() { + return css` + ha-form { + width: 100%; + } + + ha-icon-button { + align-self: center; + } + + .card-config { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + } + + .config-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + } + + .config-header.sub-header { + margin-top: 24px; + } + + ha-icon { + padding-bottom: 2px; + position: relative; + top: -4px; + right: 1px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'energy-period-selector-editor': EnergyPeriodSelectorEditor; + } +}