This commit is contained in:
karwosts 2024-04-27 21:22:15 +02:00 committed by GitHub
commit 3d0d0a7063
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1397 additions and 17 deletions

View File

@ -12,6 +12,8 @@ export class HaBooleanSelector extends LitElement {
@property({ type: Boolean }) public value = false;
@property() public placeholder?: any;
@property() public label?: string;
@property() public helper?: string;
@ -22,7 +24,7 @@ export class HaBooleanSelector extends LitElement {
return html`
<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
.checked=${this.value ?? this.placeholder === true}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>

View File

@ -13,12 +13,17 @@ import { ImageEntity, computeImageUrl } from "../../../data/image";
import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
import { PictureElementsCardConfig } from "./types";
@customElement("hui-picture-elements-card")
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-picture-elements-card-editor");
return document.createElement("hui-picture-elements-card-editor");
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _elements?: LovelaceElement[];

View File

@ -23,6 +23,7 @@ import {
LovelaceCardConstructor,
LovelaceCardFeature,
LovelaceCardFeatureConstructor,
LovelaceElementConstructor,
LovelaceHeaderFooter,
LovelaceHeaderFooterConstructor,
LovelaceRowConstructor,
@ -44,7 +45,7 @@ interface CreateElementConfigTypes {
element: {
config: LovelaceElementConfig;
element: LovelaceElement;
constructor: unknown;
constructor: LovelaceElementConstructor;
};
row: {
config: LovelaceRowConfig;

View File

@ -0,0 +1,42 @@
import "../elements/hui-conditional-element";
import "../elements/hui-icon-element";
import "../elements/hui-image-element";
import "../elements/hui-service-button-element";
import "../elements/hui-state-badge-element";
import "../elements/hui-state-icon-element";
import "../elements/hui-state-label-element";
import { LovelaceElementConfig } from "../elements/types";
import {
createLovelaceElement,
getLovelaceElementClass,
} from "./create-element-base";
const ALWAYS_LOADED_TYPES = new Set([
"conditional",
"icon",
"image",
"service-button",
"state-badge",
"state-icon",
"state-label",
]);
const LAZY_LOAD_TYPES = {};
export const createPictureElementElement = (config: LovelaceElementConfig) =>
createLovelaceElement(
"element",
config,
ALWAYS_LOADED_TYPES,
LAZY_LOAD_TYPES,
undefined, // DOMAIN_TO_ELEMENT_TYPE,
undefined
);
export const getPictureElementClass = (type: string) =>
getLovelaceElementClass(
type,
"element",
ALWAYS_LOADED_TYPES,
LAZY_LOAD_TYPES
);

View File

@ -0,0 +1,167 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
any,
array,
assert,
literal,
object,
optional,
string,
} from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../../common/dom/fire_event";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import { LovelacePictureElementEditor } from "../../../types";
import {
ConditionalElementConfig,
LovelaceElementConfig,
} from "../../../elements/types";
import "../../conditions/ha-card-conditions-editor";
import "../../hui-picture-elements-card-row-editor";
import { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
import { EditSubElementEvent, SubElementEditorConfig } from "../../types";
import "../../hui-sub-element-editor";
import { SchemaUnion } from "../../../../../components/ha-form/types";
const conditionalElementConfigStruct = object({
type: literal("conditional"),
conditions: optional(array(any())),
elements: optional(array(any())),
title: optional(string()),
});
const SCHEMA = [{ name: "title", selector: { text: {} } }] as const;
@customElement("hui-conditional-element-editor")
export class HuiConditionalElementEditor
extends LitElement
implements LovelacePictureElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ConditionalElementConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
public setConfig(config: ConditionalElementConfig): void {
assert(config, conditionalElementConfigStruct);
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
@go-back=${this._goBack}
@config-changed=${this._handleSubElementChanged}
>
</hui-sub-element-editor>
`;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._formChanged}
></ha-form>
<ha-card-conditions-editor
.hass=${this.hass}
.conditions=${this._config.conditions || []}
@value-changed=${this._conditionChanged}
>
</ha-card-conditions-editor>
<hui-picture-elements-card-row-editor
.hass=${this.hass}
.elements=${this._config.elements || []}
@elements-changed=${this._elementsChanged}
@edit-detail-element=${this._editDetailElement}
></hui-picture-elements-card-row-editor>
`;
}
private _formChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
if (!this._config) {
return;
}
const conditions = ev.detail.value;
this._config = { ...this._config, conditions };
fireEvent(this, "config-changed", { config: this._config });
}
private _elementsChanged(ev: CustomEvent): void {
ev.stopPropagation();
const config = {
...this._config,
elements: ev.detail.elements as LovelaceElementConfig[],
} as LovelaceCardConfig;
fireEvent(this, "config-changed", { config });
}
private _handleSubElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const configValue = this._subElementEditorConfig?.type;
const value = ev.detail.config;
if (configValue === "element") {
const newConfigElements = this._config.elements!.concat();
if (!value) {
newConfigElements.splice(this._subElementEditorConfig!.index!, 1);
this._goBack();
} else {
newConfigElements[this._subElementEditorConfig!.index!] = value;
}
this._config = { ...this._config!, elements: newConfigElements };
}
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _goBack(ev?): void {
ev?.stopPropagation();
this._subElementEditorConfig = undefined;
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) ||
schema.name;
}
declare global {
interface HTMLElementTagNameMap {
"hui-conditional-element-editor": HuiConditionalElementEditor;
}
}

View File

@ -0,0 +1,88 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import { LovelacePictureElementEditor } from "../../../types";
import { IconElementConfig } from "../../../elements/types";
import { actionConfigStruct } from "../../structs/action-struct";
const iconElementConfigStruct = object({
type: literal("icon"),
entity: optional(string()),
icon: optional(string()),
style: optional(any()),
title: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
});
const SCHEMA = [
{ name: "icon", selector: { icon: {} } },
{ name: "title", selector: { text: {} } },
{ name: "entity", selector: { entity: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
},
{ name: "style", selector: { object: {} } },
] as const;
@customElement("hui-icon-element-editor")
export class HuiIconElementEditor
extends LitElement
implements LovelacePictureElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: IconElementConfig;
public setConfig(config: IconElementConfig): void {
assert(config, iconElementConfigStruct);
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) ||
schema.name;
}
declare global {
interface HTMLElementTagNameMap {
"hui-icon-element-editor": HuiIconElementEditor;
}
}

View File

@ -0,0 +1,103 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import { LovelacePictureElementEditor } from "../../../types";
import { ImageElementConfig } from "../../../elements/types";
import { actionConfigStruct } from "../../structs/action-struct";
const imageElementConfigStruct = object({
type: literal("image"),
entity: optional(string()),
image: optional(string()),
style: optional(any()),
title: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
camera_image: optional(string()),
camera_view: optional(string()),
state_image: optional(any()),
filter: optional(string()),
state_filter: optional(any()),
aspect_ratio: optional(string()),
});
const SCHEMA = [
{ name: "entity", selector: { entity: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
},
{ name: "image", selector: { text: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "camera_view",
selector: { select: { options: ["auto", "live"] } },
},
{ name: "state_image", selector: { object: {} } },
{ name: "filter", selector: { text: {} } },
{ name: "state_filter", selector: { object: {} } },
{ name: "aspect_ratio", selector: { text: {} } },
{ name: "style", selector: { object: {} } },
] as const;
@customElement("hui-image-element-editor")
export class HuiImageElementEditor
extends LitElement
implements LovelacePictureElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ImageElementConfig;
public setConfig(config: ImageElementConfig): void {
assert(config, imageElementConfigStruct);
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) ||
schema.name;
}
declare global {
interface HTMLElementTagNameMap {
"hui-image-element-editor": HuiImageElementEditor;
}
}

View File

@ -0,0 +1,79 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import { LovelacePictureElementEditor } from "../../../types";
import { ServiceButtonElementConfig } from "../../../elements/types";
// import { UiAction } from "../../components/hui-action-editor";
const serviceButtonElementConfigStruct = object({
type: literal("service-button"),
style: optional(any()),
title: optional(string()),
service: optional(string()),
service_data: optional(any()),
});
const SCHEMA = [
{ name: "title", selector: { text: {} } },
/* {
name: "service",
selector: {
ui_action: { actions: ["call-service"] as UiAction[] },
},
}, */
{ name: "service", selector: { text: {} } },
{ name: "service_data", selector: { object: {} } },
{ name: "style", selector: { object: {} } },
] as const;
@customElement("hui-service-button-element-editor")
export class HuiServiceButtonElementEditor
extends LitElement
implements LovelacePictureElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ServiceButtonElementConfig;
public setConfig(config: ServiceButtonElementConfig): void {
assert(config, serviceButtonElementConfigStruct);
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) ||
schema.name;
}
declare global {
interface HTMLElementTagNameMap {
"hui-service-button-element-editor": HuiServiceButtonElementEditor;
}
}

View File

@ -0,0 +1,86 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import { LovelacePictureElementEditor } from "../../../types";
import { StateBadgeElementConfig } from "../../../elements/types";
import { actionConfigStruct } from "../../structs/action-struct";
const stateBadgeElementConfigStruct = object({
type: literal("state-badge"),
entity: optional(string()),
style: optional(any()),
title: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
});
const SCHEMA = [
{ name: "entity", selector: { entity: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
},
{ name: "style", selector: { object: {} } },
] as const;
@customElement("hui-state-badge-element-editor")
export class HuiStateBadgeElementEditor
extends LitElement
implements LovelacePictureElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateBadgeElementConfig;
public setConfig(config: StateBadgeElementConfig): void {
assert(config, stateBadgeElementConfigStruct);
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) ||
schema.name;
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-badge-element-editor": HuiStateBadgeElementEditor;
}
}

View File

@ -0,0 +1,98 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
any,
assert,
boolean,
literal,
object,
optional,
string,
} from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import { LovelacePictureElementEditor } from "../../../types";
import { StateIconElementConfig } from "../../../elements/types";
import { actionConfigStruct } from "../../structs/action-struct";
const stateIconElementConfigStruct = object({
type: literal("state-icon"),
entity: optional(string()),
icon: optional(string()),
state_color: optional(boolean()),
style: optional(any()),
title: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
});
const SCHEMA = [
{ name: "entity", selector: { entity: {} } },
{ name: "icon", selector: { icon: {} } },
{ name: "title", selector: { text: {} } },
{ name: "state_color", default: true, selector: { boolean: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
},
{ name: "style", selector: { object: {} } },
] as const;
@customElement("hui-state-icon-element-editor")
export class HuiStateIconElementEditor
extends LitElement
implements LovelacePictureElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateIconElementConfig;
public setConfig(config: StateIconElementConfig): void {
assert(config, stateIconElementConfigStruct);
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) ||
schema.name;
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-icon-element-editor": HuiStateIconElementEditor;
}
}

View File

@ -0,0 +1,98 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-form/ha-form";
import { LovelacePictureElementEditor } from "../../../types";
import { StateLabelElementConfig } from "../../../elements/types";
import { actionConfigStruct } from "../../structs/action-struct";
const stateLabelElementConfigStruct = object({
type: literal("state-label"),
entity: optional(string()),
attribute: optional(string()),
prefix: optional(string()),
suffix: optional(string()),
style: optional(any()),
title: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
});
const SCHEMA = [
{ name: "entity", required: true, selector: { entity: {} } },
{
name: "attribute",
selector: { attribute: {} },
context: {
filter_entity: "entity",
},
},
{ name: "prefix", selector: { text: {} } },
{ name: "suffix", selector: { text: {} } },
{ name: "title", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {},
},
},
{
name: "hold_action",
selector: {
ui_action: {},
},
},
{ name: "style", selector: { object: {} } },
] as const;
@customElement("hui-state-label-element-editor")
export class HuiStateLabelElementEditor
extends LitElement
implements LovelacePictureElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateLabelElementConfig;
public setConfig(config: StateLabelElementConfig): void {
assert(config, stateLabelElementConfigStruct);
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) ||
schema.name;
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-label-element-editor": HuiStateLabelElementEditor;
}
}

View File

@ -0,0 +1,214 @@
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
any,
array,
assert,
assign,
object,
optional,
string,
type,
} from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon";
import "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
import type { PictureElementsCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "../hui-picture-elements-card-row-editor";
import { LovelaceElementConfig } from "../../elements/types";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { LocalizeFunc } from "../../../../common/translations/localize";
const genericElementConfigStruct = type({
type: string(),
});
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
image: optional(string()),
camera_image: optional(string()),
camera_view: optional(string()),
elements: array(genericElementConfigStruct),
title: optional(string()),
state_filter: optional(any()),
theme: optional(string()),
dark_mode_image: optional(string()),
dark_mode_filter: optional(any()),
})
);
@customElement("hui-picture-elements-card-editor")
export class HuiPictureElementsCardEditor
extends LitElement
implements LovelaceCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: PictureElementsCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
public setConfig(config: PictureElementsCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "",
type: "expandable",
title: localize(
"ui.panel.lovelace.editor.card.picture-elements.card_options"
),
schema: [
{ name: "title", selector: { text: {} } },
{ name: "image", selector: { text: {} } },
{ name: "dark_mode_image", selector: { text: {} } },
{
name: "camera_image",
selector: { entity: { domain: "camera" } },
},
{
name: "camera_view",
selector: { select: { options: ["auto", "live"] } },
},
{ name: "theme", selector: { theme: {} } },
{ name: "state_filter", selector: { object: {} } },
{ name: "dark_mode_filter", selector: { object: {} } },
],
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
@go-back=${this._goBack}
@config-changed=${this._handleSubElementChanged}
>
</hui-sub-element-editor>
`;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._formChanged}
></ha-form>
<hui-picture-elements-card-row-editor
.hass=${this.hass}
.elements=${this._config.elements}
@elements-changed=${this._elementsChanged}
@edit-detail-element=${this._editDetailElement}
></hui-picture-elements-card-row-editor>
`;
}
private _formChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _elementsChanged(ev: CustomEvent): void {
ev.stopPropagation();
const config = {
...this._config,
elements: ev.detail.elements as LovelaceElementConfig[],
} as LovelaceCardConfig;
fireEvent(this, "config-changed", { config });
}
private _handleSubElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const configValue = this._subElementEditorConfig?.type;
const value = ev.detail.config;
if (configValue === "element") {
const newConfigElements = this._config.elements!.concat();
if (!value) {
newConfigElements.splice(this._subElementEditorConfig!.index!, 1);
this._goBack();
} else {
newConfigElements[this._subElementEditorConfig!.index!] = value;
}
this._config = { ...this._config!, elements: newConfigElements };
}
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _computeLabelCallback = (schema) => {
switch (schema.name) {
case "dark_mode_image":
case "state_filter":
case "dark_mode_filter":
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.picture-elements.${schema.name}`
) || schema.name
);
default:
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) || schema.name
);
}
};
static get styles(): CSSResultGroup {
return [configElementStyle];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-picture-elements-card-editor": HuiPictureElementsCardEditor;
}
}

View File

@ -23,6 +23,7 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceRowConfig } from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LovelaceElementConfig } from "../elements/types";
import type {
LovelaceConfigForm,
LovelaceGenericElementEditor,
@ -38,7 +39,8 @@ export interface ConfigChangedEvent {
| LovelaceRowConfig
| LovelaceHeaderFooterConfig
| LovelaceCardFeatureConfig
| LovelaceStrategyConfig;
| LovelaceStrategyConfig
| LovelaceElementConfig;
error?: string;
guiModeAvailable?: boolean;
}

View File

@ -0,0 +1,251 @@
import { mdiClose, mdiPencil, mdiContentDuplicate } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-select";
import type { HaSelect } from "../../../components/ha-select";
import {
ConditionalElementConfig,
IconElementConfig,
ImageElementConfig,
LovelaceElementConfig,
ServiceButtonElementConfig,
StateBadgeElementConfig,
StateIconElementConfig,
StateLabelElementConfig,
} from "../elements/types";
declare global {
interface HASSDomEvents {
"elements-changed": {
elements: any[];
};
}
}
const elementTypes: string[] = [
"state-badge",
"state-icon",
"state-label",
"service-button",
"icon",
"image",
"conditional",
];
@customElement("hui-picture-elements-card-row-editor")
export class HuiPictureElementsCardRowEditor extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public elements?: LovelaceElementConfig[];
@query("ha-select") private _select!: HaSelect;
protected render() {
if (!this.elements || !this.hass) {
return nothing;
}
return html`
<h3>
${this.hass.localize(
"ui.panel.lovelace.editor.card.picture-elements.elements"
)}
</h3>
<div class="elements">
${this.elements.map(
(element, index) => html`
<div class="element">
${element.type
? html`
<div class="element-row">
<div>
<span>
${this.hass?.localize(
`ui.panel.lovelace.editor.card.picture-elements.element_types.${element.type}`
) || element.type}
</span>
<span class="secondary"
>${this._getSecondaryDescription(element)}</span
>
</div>
</div>
`
: nothing}
<ha-icon-button
.label=${this.hass!.localize("ui.common.clear")}
.path=${mdiClose}
class="remove-icon"
.index=${index}
@click=${this._removeRow}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize("ui.common.edit")}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editRow}
></ha-icon-button>
<ha-icon-button
.label=${"ui.common.duplicate"}
.path=${mdiContentDuplicate}
class="duplicate-icon"
.index=${index}
@click=${this._duplicateRow}
></ha-icon-button>
</div>
`
)}
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.picture-elements.new_element"
)}
.value=${""}
@closed=${stopPropagation}
@selected=${this._addElement}
>
${elementTypes.map(
(element) => html`
<mwc-list-item .value=${element}
>${this.hass?.localize(
`ui.panel.lovelace.editor.card.picture-elements.element_types.${element}`
)}</mwc-list-item
>
`
)}
</ha-select>
</div>
`;
}
private _getSecondaryDescription(element: LovelaceElementConfig): string {
switch (element.type) {
case "icon":
return element.title ?? (element as IconElementConfig).icon ?? "";
case "state-badge":
case "state-icon":
case "state-label":
return (
element.title ??
(
element as
| StateBadgeElementConfig
| StateIconElementConfig
| StateLabelElementConfig
).entity ??
""
);
case "service-button":
return (
element.title ?? (element as ServiceButtonElementConfig).service ?? ""
);
case "image":
return (
element.title ??
(element as ImageElementConfig).image ??
(element as ImageElementConfig).camera_image ??
""
);
case "conditional":
return (
element.title ??
`${((element as ConditionalElementConfig).elements || []).length.toString()} ${this.hass?.localize("ui.panel.lovelace.editor.card.picture-elements.elements")}`
);
}
return "Unknown type";
}
private async _addElement(ev): Promise<void> {
const value = ev.target!.value;
if (value === "") {
return;
}
const newElements = this.elements!.concat({
type: value! as string,
...(value !== "conditional"
? {
style: {
top: "50%",
left: "50%",
},
}
: {}),
} as LovelaceElementConfig);
fireEvent(this, "elements-changed", { elements: newElements });
this._select.select(-1);
}
private _removeRow(ev: CustomEvent): void {
const index = (ev.currentTarget as any).index;
const newElements = this.elements!.concat();
newElements.splice(index, 1);
fireEvent(this, "elements-changed", { elements: newElements });
}
private _editRow(ev: CustomEvent): void {
const index = (ev.currentTarget as any).index;
fireEvent(this, "edit-detail-element", {
subElementConfig: {
index,
type: "element",
elementConfig: this.elements![index],
},
});
}
private _duplicateRow(ev: CustomEvent): void {
const index = (ev.currentTarget as any).index;
const newElements = [...this.elements!, this.elements![index]];
fireEvent(this, "elements-changed", { elements: newElements });
}
static get styles(): CSSResultGroup {
return css`
.element {
display: flex;
align-items: center;
}
.element-row {
height: 60px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-grow: 1;
}
.element-row div {
display: flex;
flex-direction: column;
}
.remove-icon,
.edit-icon,
.duplicate-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-picture-elements-card-row-editor": HuiPictureElementsCardRowEditor;
}
}

View File

@ -1,6 +1,13 @@
import "@material/mwc-button";
import { mdiCodeBraces, mdiListBoxOutline } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-icon-button";
@ -13,6 +20,7 @@ import "./header-footer-editor/hui-header-footer-element-editor";
import type { HuiElementEditor } from "./hui-element-editor";
import "./feature-editor/hui-card-feature-element-editor";
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
import "./picture-element-editor/hui-picture-element-element-editor";
declare global {
interface HASSDomEvents {
@ -95,7 +103,18 @@ export class HuiSubElementEditor extends LitElement {
@GUImode-changed=${this._handleGUIModeChanged}
></hui-card-feature-element-editor>
`
: ""}
: this.config.type === "element"
? html`
<hui-picture-element-element-editor
class="editor"
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.context}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-picture-element-element-editor>
`
: nothing}
`;
}

View File

@ -0,0 +1,31 @@
import { customElement } from "lit/decorators";
import { LovelaceElementConfig } from "../../elements/types";
import type { LovelacePictureElementEditor } from "../../types";
import { HuiElementEditor } from "../hui-element-editor";
import { getPictureElementClass } from "../../create-element/create-picture-element";
@customElement("hui-picture-element-element-editor")
export class HuiPictureElementElementEditor extends HuiElementEditor<LovelaceElementConfig> {
protected get configElementType(): string | undefined {
return this.value?.type;
}
protected async getConfigElement(): Promise<
LovelacePictureElementEditor | undefined
> {
const elClass = await getPictureElementClass(this.configElementType!);
// Check if a GUI editor exists
if (elClass && elClass.getConfigElement) {
return elClass.getConfigElement();
}
return undefined;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-picture-element-element-editor": HuiPictureElementElementEditor;
}
}

View File

@ -7,6 +7,7 @@ import {
import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LovelaceElementConfig } from "../elements/types";
export interface YamlChangedEvent extends Event {
detail: {
@ -79,8 +80,9 @@ export interface SubElementEditorConfig {
elementConfig?:
| LovelaceRowConfig
| LovelaceHeaderFooterConfig
| LovelaceCardFeatureConfig;
type: "header" | "footer" | "row" | "feature";
| LovelaceCardFeatureConfig
| LovelaceElementConfig;
type: "header" | "footer" | "row" | "feature" | "element";
}
export interface EditSubElementEvent {

View File

@ -4,6 +4,7 @@ import {
checkConditionsMet,
validateConditionalConfig,
} from "../common/validate-condition";
import { LovelacePictureElementEditor } from "../types";
import {
ConditionalElementConfig,
LovelaceElement,
@ -11,6 +12,13 @@ import {
} from "./types";
class HuiConditionalElement extends HTMLElement implements LovelaceElement {
public static async getConfigElement(): Promise<LovelacePictureElementEditor> {
await import(
"../editor/config-elements/elements/hui-conditional-element-editor"
);
return document.createElement("hui-conditional-element-editor");
}
public _hass?: HomeAssistant;
private _config?: ConditionalElementConfig;

View File

@ -8,10 +8,16 @@ import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { IconElementConfig, LovelaceElement } from "./types";
import { LovelacePictureElementEditor } from "../types";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@customElement("hui-icon-element")
export class HuiIconElement extends LitElement implements LovelaceElement {
public static async getConfigElement(): Promise<LovelacePictureElementEditor> {
await import("../editor/config-elements/elements/hui-icon-element-editor");
return document.createElement("hui-icon-element-editor");
}
public hass?: HomeAssistant;
@state() private _config?: IconElementConfig;

View File

@ -10,9 +10,15 @@ import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import "../components/hui-image";
import { ImageElementConfig, LovelaceElement } from "./types";
import { LovelacePictureElementEditor } from "../types";
@customElement("hui-image-element")
export class HuiImageElement extends LitElement implements LovelaceElement {
public static async getConfigElement(): Promise<LovelacePictureElementEditor> {
await import("../editor/config-elements/elements/hui-image-element-editor");
return document.createElement("hui-image-element-editor");
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ImageElementConfig;

View File

@ -3,12 +3,20 @@ import { customElement, state } from "lit/decorators";
import "../../../components/buttons/ha-call-service-button";
import { HomeAssistant } from "../../../types";
import { LovelaceElement, ServiceButtonElementConfig } from "./types";
import { LovelacePictureElementEditor } from "../types";
@customElement("hui-service-button-element")
export class HuiServiceButtonElement
extends LitElement
implements LovelaceElement
{
public static async getConfigElement(): Promise<LovelacePictureElementEditor> {
await import(
"../editor/config-elements/elements/hui-service-button-element-editor"
);
return document.createElement("hui-service-button-element-editor");
}
public hass?: HomeAssistant;
@state() private _config?: ServiceButtonElementConfig;

View File

@ -12,12 +12,20 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
import { LovelaceElement, StateBadgeElementConfig } from "./types";
import { LovelacePictureElementEditor } from "../types";
@customElement("hui-state-badge-element")
export class HuiStateBadgeElement
extends LitElement
implements LovelaceElement
{
public static async getConfigElement(): Promise<LovelacePictureElementEditor> {
await import(
"../editor/config-elements/elements/hui-state-badge-element-editor"
);
return document.createElement("hui-state-badge-element-editor");
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateBadgeElementConfig;
@ -44,7 +52,7 @@ export class HuiStateBadgeElement
if (!stateObj) {
return html`
<hui-warning-element
.label=${createEntityNotFoundWarning(this.hass, this._config.entity)}
.label=${createEntityNotFoundWarning(this.hass, this._config.entity!)}
></hui-warning-element>
`;
}

View File

@ -18,10 +18,18 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
import { LovelaceElement, StateIconElementConfig } from "./types";
import { LovelacePictureElementEditor } from "../types";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@customElement("hui-state-icon-element")
export class HuiStateIconElement extends LitElement implements LovelaceElement {
public static async getConfigElement(): Promise<LovelacePictureElementEditor> {
await import(
"../editor/config-elements/elements/hui-state-icon-element-editor"
);
return document.createElement("hui-state-icon-element-editor");
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateIconElementConfig;
@ -52,7 +60,7 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
if (!stateObj) {
return html`
<hui-warning-element
.label=${createEntityNotFoundWarning(this.hass, this._config.entity)}
.label=${createEntityNotFoundWarning(this.hass, this._config.entity!)}
></hui-warning-element>
`;
}

View File

@ -18,9 +18,17 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
import { LovelaceElement, StateLabelElementConfig } from "./types";
import { LovelacePictureElementEditor } from "../types";
@customElement("hui-state-label-element")
class HuiStateLabelElement extends LitElement implements LovelaceElement {
public static async getConfigElement(): Promise<LovelacePictureElementEditor> {
await import(
"../editor/config-elements/elements/hui-state-label-element-editor"
);
return document.createElement("hui-state-label-element-editor");
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateLabelElementConfig;
@ -47,7 +55,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
if (!stateObj) {
return html`
<hui-warning-element
.label=${createEntityNotFoundWarning(this.hass, this._config.entity)}
.label=${createEntityNotFoundWarning(this.hass, this._config.entity!)}
></hui-warning-element>
`;
}

View File

@ -25,6 +25,7 @@ export interface LovelaceElement extends HTMLElement {
export interface ConditionalElementConfig extends LovelaceElementConfigBase {
conditions: Condition[];
elements: LovelaceElementConfigBase[];
title?: string;
}
export interface IconElementConfig extends LovelaceElementConfigBase {
@ -33,7 +34,8 @@ export interface IconElementConfig extends LovelaceElementConfigBase {
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
icon: string;
icon?: string;
title?: string;
}
export interface ImageElementConfig extends LovelaceElementConfigBase {
@ -51,6 +53,7 @@ export interface ImageElementConfig extends LovelaceElementConfigBase {
filter?: string;
state_filter?: string;
aspect_ratio?: string;
title?: string;
}
export interface ServiceButtonElementConfig extends LovelaceElementConfigBase {
@ -60,7 +63,7 @@ export interface ServiceButtonElementConfig extends LovelaceElementConfigBase {
}
export interface StateBadgeElementConfig extends LovelaceElementConfigBase {
entity: string;
entity?: string;
title?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
@ -68,20 +71,22 @@ export interface StateBadgeElementConfig extends LovelaceElementConfigBase {
}
export interface StateIconElementConfig extends LovelaceElementConfigBase {
entity: string;
entity?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
icon?: string;
state_color?: boolean;
title?: string;
}
export interface StateLabelElementConfig extends LovelaceElementConfigBase {
entity: string;
entity?: string;
attribute?: string;
prefix?: string;
suffix?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
title?: string;
}

View File

@ -12,6 +12,7 @@ import { Constructor, HomeAssistant } from "../../types";
import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
import { LovelaceCardFeatureConfig } from "./card-features/types";
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
declare global {
// eslint-disable-next-line
@ -90,6 +91,11 @@ export interface LovelaceRowConstructor extends Constructor<LovelaceRow> {
getConfigElement?: () => LovelaceRowEditor;
}
export interface LovelaceElementConstructor
extends Constructor<LovelaceElement> {
getConfigElement?: () => LovelacePictureElementEditor;
}
export interface LovelaceHeaderFooter extends HTMLElement {
hass?: HomeAssistant;
type: "header" | "footer";
@ -110,6 +116,11 @@ export interface LovelaceRowEditor extends LovelaceGenericElementEditor {
setConfig(config: LovelaceRowConfig): void;
}
export interface LovelacePictureElementEditor
extends LovelaceGenericElementEditor {
setConfig(config: LovelaceElementConfig): void;
}
export interface LovelaceGenericElementEditor<C = any> extends HTMLElement {
hass?: HomeAssistant;
lovelace?: LovelaceConfig;

View File

@ -5865,7 +5865,22 @@
},
"picture-elements": {
"name": "Picture elements",
"description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even services! On an image based on coordinates."
"description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even services! On an image based on coordinates.",
"card_options": "Card Options",
"elements": "Elements",
"new_element": "Add new element",
"dark_mode_image": "Dark mode image path",
"state_filter": "State filter",
"dark_mode_filter": "Dark mode state filter",
"element_types": {
"state-badge": "State badge",
"state-icon": "State icon",
"state-label": "State label",
"service-button": "Service call button",
"icon": "Icon",
"image": "Image",
"conditional": "Conditional"
}
},
"picture-entity": {
"name": "Picture entity",
@ -5931,6 +5946,14 @@
"twice_daily": "Twice daily"
}
},
"elements": {
"style": "Style",
"prefix": "Prefix",
"suffix": "Suffix",
"state_image": "State image",
"filter": "Filter",
"state_filter": "[%key:ui::panel::lovelace::editor::card::picture-elements::state_filter%]"
},
"features": {
"name": "Features",
"not_compatible": "Not compatible",
@ -6131,7 +6154,8 @@
"header": "Header editor",
"footer": "Footer editor",
"row": "Entity row editor",
"feature": "Feature editor"
"feature": "Feature editor",
"element": "Element editor"
}
}
},