Skip to content

Commit

Permalink
Picture Elements Visual editor (#19718)
Browse files Browse the repository at this point in the history
* preliminary edits

* more functional prototype

* all types implemented

* mostly style and localization updates

* fix empty conditional case

* Move getConfigElement to elements themselves

* drop unneeded imports

* move struct validation to individual element editors

* description for unknown types

* Update src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts

Co-authored-by: Simon Lamon <[email protected]>

* Update src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts

Co-authored-by: Simon Lamon <[email protected]>

* Update src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts

Co-authored-by: Simon Lamon <[email protected]>

* Update src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts

Co-authored-by: Simon Lamon <[email protected]>

* Update hui-picture-elements-card-row-editor.ts

* Fix merge mistake

* Update src/panels/lovelace/create-element/create-picture-element.ts

remove comment

Co-authored-by: Bram Kragten <[email protected]>

---------

Co-authored-by: Simon Lamon <[email protected]>
Co-authored-by: Bram Kragten <[email protected]>
  • Loading branch information
3 people authored Jul 31, 2024
1 parent d0e61ca commit e05c664
Show file tree
Hide file tree
Showing 27 changed files with 1,400 additions and 16 deletions.
4 changes: 3 additions & 1 deletion src/components/ha-selector/ha-selector-boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>
Expand Down
7 changes: 6 additions & 1 deletion src/panels/lovelace/cards/hui-picture-elements-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ 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";
import { PersonEntity } from "../../../data/person";

@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[];
Expand Down
3 changes: 2 additions & 1 deletion src/panels/lovelace/create-element/create-element-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
LovelaceCardConstructor,
LovelaceCardFeature,
LovelaceCardFeatureConstructor,
LovelaceElementConstructor,
LovelaceHeaderFooter,
LovelaceHeaderFooterConstructor,
LovelaceRowConstructor,
Expand All @@ -44,7 +45,7 @@ interface CreateElementConfigTypes {
element: {
config: LovelaceElementConfig;
element: LovelaceElement;
constructor: unknown;
constructor: LovelaceElementConstructor;
};
row: {
config: LovelaceRowConfig;
Expand Down
42 changes: 42 additions & 0 deletions src/panels/lovelace/create-element/create-picture-element.ts
Original file line number Diff line number Diff line change
@@ -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,
undefined
);

export const getPictureElementClass = (type: string) =>
getLovelaceElementClass(
type,
"element",
ALWAYS_LOADED_TYPES,
LAZY_LOAD_TYPES
);
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit e05c664

Please sign in to comment.