Compare commits

...

10 Commits

Author SHA1 Message Date
Aidan Timson 9e5605e6c0 Restructure 2026-05-11 14:54:17 +01:00
Aidan Timson 346d41fde2 Remove display options 2026-05-11 14:51:48 +01:00
Aidan Timson cc251de994 Only show header on mobile 2026-05-11 14:47:37 +01:00
Aidan Timson a38507a386 Remove header 2026-05-11 14:46:44 +01:00
Aidan Timson 541ce87e8a Bottom 2026-05-11 14:45:42 +01:00
Aidan Timson 8502b1f87e Fix close 2026-05-11 14:41:57 +01:00
Aidan Timson 123ecc52f8 Remove title option 2026-05-11 14:34:08 +01:00
Aidan Timson 6ef70def5d Popover refactor 2026-05-11 14:30:31 +01:00
Aidan Timson 98f0c977b6 Editor UI 2026-05-11 14:01:26 +01:00
Aidan Timson 651192dc49 Setup 2026-05-11 12:41:39 +01:00
12 changed files with 671 additions and 277 deletions
+9
View File
@@ -1,4 +1,5 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { LovelaceCardConfig } from "./card";
export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
@@ -37,6 +38,13 @@ export interface AssistActionConfig extends BaseActionConfig {
start_listening?: boolean;
}
export interface ShowPopupActionConfig extends BaseActionConfig {
action: "show-popup";
desktop_mode?: "popover" | "dialog";
mobile_mode?: "bottom-sheet" | "dialog";
cards: LovelaceCardConfig[];
}
export interface NoActionConfig extends BaseActionConfig {
action: "none";
}
@@ -69,5 +77,6 @@ export type ActionConfig =
| UrlActionConfig
| MoreInfoActionConfig
| AssistActionConfig
| ShowPopupActionConfig
| NoActionConfig
| CustomActionConfig;
@@ -69,7 +69,12 @@ export class HuiShortcutBadge extends LitElement implements LovelaceBadge {
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
handleAction(
ev.currentTarget as HTMLElement,
this.hass!,
this._config!,
ev.detail.action!
);
}
private get _hasAction() {
@@ -95,7 +95,12 @@ export class HuiShortcutCard extends LitElement implements LovelaceCard {
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
handleAction(
ev.currentTarget as HTMLElement,
this.hass!,
this._config!,
ev.detail.action!
);
}
private get _hasCardAction() {
@@ -9,6 +9,9 @@ import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import { toggleEntity } from "./entity/toggle-entity";
const loadLovelacePopupDialog = () =>
import("../dialogs/dialog-lovelace-popup");
declare global {
interface HASSDomEvents {
"ll-custom": ActionConfig;
@@ -183,6 +186,27 @@ export const handleAction = async (
});
break;
}
case "show-popup": {
if (!actionConfig.cards?.length) {
showToast(node, {
message: hass.localize(
"ui.panel.lovelace.cards.actions.no_popup_cards"
),
});
forwardHaptic(node, "failure");
return;
}
fireEvent(node, "show-dialog", {
dialogTag: "dialog-lovelace-popup",
dialogImport: loadLovelacePopupDialog,
dialogAnchor: node,
dialogParams: {
hass,
cards: actionConfig.cards,
},
});
break;
}
case "fire-dom-event": {
fireEvent(node, "ll-custom", actionConfig);
}
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { refine } from "superstruct";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-assist-pipeline-picker";
import type {
@@ -20,14 +21,18 @@ import type {
NavigateActionConfig,
UrlActionConfig,
} from "../../../data/lovelace/config/action";
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
import type { ServiceAction } from "../../../data/script";
import type { HomeAssistant } from "../../../types";
import "../editor/card-editor/hui-card-list-editor";
import type { CardsChangedEvent } from "../editor/card-editor/hui-card-list-editor";
export type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
export interface ActionRelatedContext {
entity_id?: string;
area_id?: string;
lovelace?: LovelaceConfig;
}
export const ACTION_RELATED_CONTEXT = {
@@ -235,6 +240,16 @@ export class HuiActionEditor extends LitElement {
</ha-form>
`
: nothing}
${this.config?.action === "show-popup"
? html`
<hui-card-list-editor
.hass=${this.hass}
.lovelace=${this.context?.lovelace}
.cards=${this.config.cards ?? []}
@cards-changed=${this._popupCardsChanged}
></hui-card-list-editor>
`
: nothing}
`;
}
@@ -273,6 +288,12 @@ export class HuiActionEditor extends LitElement {
data = { navigation_path: this._navigation_path };
break;
}
case "show-popup": {
data = {
cards: this.config?.action === "show-popup" ? this.config.cards : [],
};
break;
}
}
fireEvent(this, "value-changed", {
@@ -307,7 +328,19 @@ export class HuiActionEditor extends LitElement {
});
}
private _computeFormLabel(schema: SchemaUnion<typeof ASSIST_SCHEMA>) {
private _popupCardsChanged(ev: HASSDomEvent<CardsChangedEvent>) {
ev.stopPropagation();
if (this.config?.action !== "show-popup") {
return;
}
fireEvent(this, "value-changed", {
value: { ...this.config, cards: ev.detail.cards },
});
}
private _computeFormLabel(
schema: SchemaUnion<typeof ASSIST_SCHEMA> | HaFormSchema
) {
return this.hass?.localize(
`ui.panel.lovelace.editor.action-editor.${schema.name}`
);
@@ -366,6 +399,10 @@ export class HuiActionEditor extends LitElement {
ha-service-control {
--service-control-padding: 0;
}
hui-card-list-editor {
display: block;
margin-top: 8px;
}
`;
}
@@ -0,0 +1,233 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { ADAPTIVE_DIALOG_MEDIA_QUERY } from "../../../components/ha-adaptive-dialog";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-bottom-sheet";
import { DialogMixin } from "../../../dialogs/dialog-mixin";
import "../sections/hui-section";
type WaPopoverElement = HTMLElement & {
anchor: Element | null;
};
export interface LovelacePopupDialogParams {
hass: HomeAssistant;
cards: LovelaceCardConfig[];
}
@customElement("dialog-lovelace-popup")
export class DialogLovelacePopup extends DialogMixin<LovelacePopupDialogParams>(
LitElement
) {
@property({ attribute: false }) public params?: LovelacePopupDialogParams;
@property({ attribute: false }) public override dialogAnchor?: Element;
@state() private _narrow = false;
@state() private _open = true;
@state() private _popoverOpen = false;
private _unsubMediaQuery?: () => void;
private _openPopoverAnimationFrame?: number;
connectedCallback() {
super.connectedCallback();
this._unsubMediaQuery = listenMediaQuery(
ADAPTIVE_DIALOG_MEDIA_QUERY,
(matches) => {
this._narrow = matches;
}
);
}
disconnectedCallback() {
this._cancelScheduledPopoverOpen();
this._unsubMediaQuery?.();
this._unsubMediaQuery = undefined;
super.disconnectedCallback();
}
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("params")) {
this._open = true;
this._popoverOpen = false;
}
}
protected updated() {
if (this._presentationMode !== "popover") {
this._cancelScheduledPopoverOpen();
this._popoverOpen = false;
return;
}
this._syncPopoverAnchor();
if (!this._open) {
this._cancelScheduledPopoverOpen();
this._popoverOpen = false;
return;
}
if (this._popoverOpen || this._openPopoverAnimationFrame !== undefined) {
return;
}
this._openPopoverAnimationFrame = requestAnimationFrame(() => {
this._openPopoverAnimationFrame = undefined;
this._syncPopoverAnchor();
if (this._open && this._presentationMode === "popover") {
this._popoverOpen = true;
}
});
}
private _cancelScheduledPopoverOpen() {
if (this._openPopoverAnimationFrame === undefined) {
return;
}
cancelAnimationFrame(this._openPopoverAnimationFrame);
this._openPopoverAnimationFrame = undefined;
}
private _syncPopoverAnchor() {
const popover =
this.renderRoot.querySelector<WaPopoverElement>("wa-popover");
const anchor = this.dialogAnchor ?? null;
if (popover && popover.anchor !== anchor) {
popover.anchor = anchor;
}
}
public override closeDialog(_historyState?: any): Promise<boolean> | boolean {
if (this._presentationMode === "popover") {
this._open = false;
this._popoverOpen = false;
return true;
}
return super.closeDialog(_historyState);
}
private get _presentationMode(): "popover" | "bottom-sheet" {
return this._narrow ? "bottom-sheet" : "popover";
}
protected render() {
if (!this.params) {
return nothing;
}
const presentationMode = this._presentationMode;
const popupLabel = this.params.hass.localize(
"ui.panel.lovelace.editor.action-editor.actions.show-popup"
);
const content = html`<hui-section
.hass=${this.params.hass}
.config=${{ cards: this.params.cards }}
.index=${0}
.viewIndex=${0}
import-only
></hui-section>`;
if (presentationMode === "bottom-sheet") {
return html`<ha-bottom-sheet .open=${this._open} aria-label=${popupLabel}
>${content}</ha-bottom-sheet
>`;
}
return html`<wa-popover
.open=${this._popoverOpen}
.anchor=${this.dialogAnchor ?? null}
auto-size="vertical"
auto-size-padding="16"
placement="bottom"
without-arrow
trap-focus
role="dialog"
aria-modal="true"
aria-label=${popupLabel}
@wa-show=${this._handlePopoverShow}
@wa-after-hide=${this._handlePopoverAfterHide}
>
<div class="popover-surface">${content}</div>
</wa-popover>`;
}
private _handlePopoverShow(ev: Event) {
if (ev.eventPhase === Event.AT_TARGET) {
this._open = true;
fireEvent(this, "opened");
}
}
private _handlePopoverAfterHide(ev: Event) {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
this._open = false;
this._popoverOpen = false;
fireEvent(this, "closed");
}
static styles = css`
ha-bottom-sheet {
--dialog-content-padding: var(--ha-space-4);
--ha-bottom-sheet-content-padding: var(--ha-space-4);
}
wa-popover {
--width: min(var(--ha-dialog-width-lg, 1024px), 95vw);
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
);
--wa-panel-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-3xl)
);
}
wa-popover::part(dialog)::backdrop {
background: none;
}
wa-popover::part(body) {
padding: 0;
border-color: transparent;
box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l));
min-width: var(--width);
max-width: var(--width);
max-height: calc(var(--safe-height) - var(--ha-space-20));
overflow: hidden;
color: var(--primary-text-color);
}
.popover-surface {
display: flex;
flex-direction: column;
max-height: inherit;
overflow: auto;
padding: var(--ha-space-4);
}
hui-section {
display: block;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-lovelace-popup": DialogLovelacePopup;
}
}
@@ -0,0 +1,306 @@
import {
mdiCodeBraces,
mdiContentCopy,
mdiContentCut,
mdiDelete,
mdiListBoxOutline,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { storage } from "../../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-arrow-next";
import "../../../../components/ha-icon-button-arrow-prev";
import "../../../../components/ha-tab-group";
import "../../../../components/ha-tab-group-tab";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { HomeAssistant } from "../../../../types";
import type { HuiCardElementEditor } from "./hui-card-element-editor";
import "./hui-card-element-editor";
import "./hui-card-picker";
import type { ConfigChangedEvent } from "../hui-element-editor";
import type { GUIModeChangedEvent } from "../types";
export interface CardsChangedEvent {
cards: LovelaceCardConfig[];
guiModeAvailable?: boolean;
}
declare global {
interface HASSDomEvents {
"cards-changed": CardsChangedEvent;
}
}
@customElement("hui-card-list-editor")
export class HuiCardListEditor extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public lovelace?: LovelaceConfig;
@property({ attribute: false }) public cards: LovelaceCardConfig[] = [];
@property({ type: Boolean, attribute: "show-copy-cut" })
public showCopyCut = false;
@storage({
key: "dashboardCardClipboard",
state: false,
subscribe: false,
storage: "sessionStorage",
})
protected _clipboard?: LovelaceCardConfig;
@state() private _selectedCard = 0;
@state() private _guiMode = true;
@state() private _guiModeAvailable? = true;
@query("hui-card-element-editor")
private _cardEditorEl?: HuiCardElementEditor;
private _keys = new Map<string, string>();
public focusYamlEditor() {
this._cardEditorEl?.focusYamlEditor();
}
protected render() {
const selected = this._selectedCard;
const isGuiMode = !this._cardEditorEl || this._guiMode;
return html`
<div class="card-config">
<div class="toolbar">
<ha-tab-group @wa-tab-show=${this._handleSelectedCard}>
${this.cards.map(
(_card, index) => html`
<ha-tab-group-tab
slot="nav"
.panel=${index}
.active=${index === selected}
>
${index + 1}
</ha-tab-group-tab>
`
)}
</ha-tab-group>
<ha-icon-button
@click=${this._handleAddCard}
.path=${mdiPlus}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.add_card"
)}
></ha-icon-button>
</div>
<div class="editor">
${selected < this.cards.length
? html`
<div class="card-options">
<ha-icon-button
class="gui-mode-button"
@click=${this._toggleMode}
.disabled=${!this._guiModeAvailable}
.label=${this.hass!.localize(
isGuiMode
? "ui.panel.lovelace.editor.edit_card.show_code_editor"
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
)}
.path=${isGuiMode ? mdiCodeBraces : mdiListBoxOutline}
></ha-icon-button>
<ha-icon-button-arrow-prev
.disabled=${selected === 0}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.move_before"
)}
@click=${this._handleMove}
.move=${-1}
></ha-icon-button-arrow-prev>
<ha-icon-button-arrow-next
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.move_after"
)}
.disabled=${selected === this.cards.length - 1}
@click=${this._handleMove}
.move=${1}
></ha-icon-button-arrow-next>
${this.showCopyCut
? html`
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
)}
.path=${mdiContentCopy}
@click=${this._handleCopyCard}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.cut"
)}
.path=${mdiContentCut}
@click=${this._handleCutCard}
></ha-icon-button>
`
: ""}
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete"
)}
.path=${mdiDelete}
@click=${this._handleDeleteCard}
></ha-icon-button>
</div>
${keyed(
this._getKey(this.cards, selected),
html`<hui-card-element-editor
.hass=${this.hass}
.value=${this.cards[selected]}
.lovelace=${this.lovelace}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-card-element-editor>`
)}
`
: html`
<hui-card-picker
.hass=${this.hass}
.lovelace=${this.lovelace}
@config-changed=${this._handleCardPicked}
></hui-card-picker>
`}
</div>
</div>
`;
}
private _getKey(cards: LovelaceCardConfig[], index: number): string {
const key = `${index}-${cards.length}`;
if (!this._keys.has(key)) {
this._keys.set(key, Math.random().toString());
}
return this._keys.get(key)!;
}
private _handleAddCard() {
this._selectedCard = this.cards.length;
}
private _handleSelectedCard(ev) {
this._guiMode = true;
this._guiModeAvailable = true;
this._selectedCard = parseInt(ev.detail.name, 10);
}
private _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) {
ev.stopPropagation();
const cards = [...this.cards];
cards[this._selectedCard] = ev.detail.config as LovelaceCardConfig;
this._fireCardsChanged(cards, ev.detail.guiModeAvailable);
}
private _handleCardPicked(ev: HASSDomEvent<ConfigChangedEvent>) {
ev.stopPropagation();
this._keys.clear();
this._fireCardsChanged([
...this.cards,
ev.detail.config as LovelaceCardConfig,
]);
}
private _handleCopyCard() {
this._clipboard = deepClone(this.cards[this._selectedCard]);
}
private _handleCutCard() {
this._handleCopyCard();
this._handleDeleteCard();
}
private _handleDeleteCard() {
const cards = [...this.cards];
cards.splice(this._selectedCard, 1);
this._selectedCard = Math.max(0, this._selectedCard - 1);
this._keys.clear();
this._fireCardsChanged(cards);
}
private _handleMove(ev: Event) {
const move = (ev.currentTarget as HTMLElement & { move: number }).move;
const target = this._selectedCard + move;
const cards = [...this.cards];
const card = cards.splice(this._selectedCard, 1)[0];
cards.splice(target, 0, card);
this._selectedCard = target;
this._keys.clear();
this._fireCardsChanged(cards);
}
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
ev.stopPropagation();
this._guiMode = ev.detail.guiMode;
this._guiModeAvailable = ev.detail.guiModeAvailable;
}
private _toggleMode(): void {
this._cardEditorEl?.toggleMode();
}
private _fireCardsChanged(
cards: LovelaceCardConfig[],
guiModeAvailable?: boolean
) {
fireEvent(this, "cards-changed", { cards, guiModeAvailable });
}
static styles = css`
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-tab-group {
flex-grow: 1;
min-width: 0;
--ha-tab-track-color: var(--card-background-color);
}
.card-options {
display: flex;
justify-content: flex-end;
width: 100%;
}
.editor {
border: 1px solid var(--divider-color);
padding: 12px;
}
@media (max-width: 450px) {
.editor {
margin: 0 -12px;
}
}
.gui-mode-button {
margin-right: auto;
margin-inline-end: auto;
margin-inline-start: initial;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-card-list-editor": HuiCardListEditor;
}
}
@@ -7,6 +7,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import { NavigationPathInfoController } from "../../../../data/navigation-path-controller";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import { ServiceInfoController } from "../../../../data/service-info-controller";
import type { HomeAssistant } from "../../../../types";
import { getShortcutCardDefaults } from "../../cards/hui-shortcut-card-defaults";
@@ -22,6 +23,7 @@ const actions: UiAction[] = [
"url",
"assist",
"perform-action",
"show-popup",
"none",
];
@@ -44,6 +46,8 @@ export class HuiShortcutBadgeEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public lovelace?: LovelaceConfig;
@state() private _config?: ShortcutBadgeConfig;
private _navInfo = new NavigationPathInfoController(this);
@@ -166,6 +170,7 @@ export class HuiShortcutBadgeEditor
.hass=${this.hass}
.data=${this._config}
.schema=${this._schema(defaults.label, defaults.icon)}
.context=${{ lovelace: this.lovelace }}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -8,6 +8,7 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import { NavigationPathInfoController } from "../../../../data/navigation-path-controller";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import { ServiceInfoController } from "../../../../data/service-info-controller";
import type { HomeAssistant } from "../../../../types";
import { getShortcutCardDefaults } from "../../cards/hui-shortcut-card-defaults";
@@ -23,6 +24,7 @@ const actions: UiAction[] = [
"url",
"assist",
"perform-action",
"show-popup",
"none",
];
@@ -47,6 +49,8 @@ export class HuiShortcutCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public lovelace?: LovelaceConfig;
@state() private _config?: ShortcutCardConfig;
private _navInfo = new NavigationPathInfoController(this);
@@ -208,6 +212,7 @@ export class HuiShortcutCardEditor
defaults.label,
defaults.icon
)}
.context=${{ lovelace: this.lovelace }}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -1,16 +1,5 @@
import {
mdiCodeBraces,
mdiContentCopy,
mdiContentCut,
mdiDelete,
mdiListBoxOutline,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import {
any,
array,
@@ -20,29 +9,22 @@ import {
optional,
string,
} from "superstruct";
import { storage } from "../../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-arrow-next";
import "../../../../components/ha-icon-button-arrow-prev";
import "../../../../components/ha-tab-group";
import "../../../../components/ha-tab-group-tab";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { HomeAssistant } from "../../../../types";
import type { StackCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../card-editor/hui-card-element-editor";
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
import "../card-editor/hui-card-picker";
import type { ConfigChangedEvent } from "../hui-element-editor";
import "../card-editor/hui-card-list-editor";
import type {
CardsChangedEvent,
HuiCardListEditor,
} from "../card-editor/hui-card-list-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { GUIModeChangedEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
@@ -69,28 +51,12 @@ export class HuiStackCardEditor
@property({ attribute: false }) public lovelace?: LovelaceConfig;
@storage({
key: "dashboardCardClipboard",
state: false,
subscribe: false,
storage: "sessionStorage",
})
protected _clipboard?: LovelaceCardConfig;
@state() protected _config?: StackCardConfig;
@state() protected _selectedCard = 0;
@state() protected _GUImode = true;
@state() protected _guiModeAvailable? = true;
protected _keys = new Map<string, string>();
protected _schema: readonly HaFormSchema[] = SCHEMA;
@query("hui-card-element-editor")
protected _cardEditorEl?: HuiCardElementEditor;
@query("hui-card-list-editor")
protected _cardListEditorEl?: HuiCardListEditor;
public setConfig(config: Readonly<StackCardConfig>): void {
assert(config, cardConfigStruct);
@@ -98,7 +64,7 @@ export class HuiStackCardEditor
}
public focusYamlEditor() {
this._cardEditorEl?.focusYamlEditor();
this._cardListEditorEl?.focusYamlEditor();
}
protected formData(): object {
@@ -109,11 +75,6 @@ export class HuiStackCardEditor
if (!this.hass || !this._config) {
return nothing;
}
const selected = this._selectedCard!;
const numcards = this._config.cards.length;
const isGuiMode = !this._cardEditorEl || this._GUImode;
return html`
<ha-form
.hass=${this.hass}
@@ -122,202 +83,29 @@ export class HuiStackCardEditor
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
<div class="toolbar">
<ha-tab-group @wa-tab-show=${this._handleSelectedCard}>
${this._config.cards.map(
(_card, i) =>
html`<ha-tab-group-tab
slot="nav"
.panel=${i}
.active=${i === selected}
>
${i + 1}
</ha-tab-group-tab>`
)}
</ha-tab-group>
<ha-icon-button
@click=${this._handleAddCard}
.path=${mdiPlus}
></ha-icon-button>
</div>
<div id="editor">
${selected < numcards
? html`
<div id="card-options">
<ha-icon-button
class="gui-mode-button"
@click=${this._toggleMode}
.disabled=${!this._guiModeAvailable}
.label=${this.hass!.localize(
isGuiMode
? "ui.panel.lovelace.editor.edit_card.show_code_editor"
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
)}
.path=${isGuiMode ? mdiCodeBraces : mdiListBoxOutline}
></ha-icon-button>
<ha-icon-button-arrow-prev
.disabled=${selected === 0}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.move_before"
)}
@click=${this._handleMove}
.move=${-1}
></ha-icon-button-arrow-prev>
<ha-icon-button-arrow-next
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.move_after"
)}
.disabled=${selected === numcards - 1}
@click=${this._handleMove}
.move=${1}
></ha-icon-button-arrow-next>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
)}
.path=${mdiContentCopy}
@click=${this._handleCopyCard}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.cut"
)}
.path=${mdiContentCut}
@click=${this._handleCutCard}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete"
)}
.path=${mdiDelete}
@click=${this._handleDeleteCard}
></ha-icon-button>
</div>
${keyed(
this._getKey(this._config.cards, selected),
html`<hui-card-element-editor
.hass=${this.hass}
.value=${this._config.cards[selected]}
.lovelace=${this.lovelace}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-card-element-editor>`
)}
`
: html`
<hui-card-picker
.hass=${this.hass}
.lovelace=${this.lovelace}
@config-changed=${this._handleCardPicked}
></hui-card-picker>
`}
</div>
</div>
<hui-card-list-editor
.hass=${this.hass}
.lovelace=${this.lovelace}
.cards=${this._config.cards}
show-copy-cut
@cards-changed=${this._cardsChanged}
></hui-card-list-editor>
`;
}
private _getKey(cards: LovelaceCardConfig[], index: number): string {
const key = `${index}-${cards.length}`;
if (!this._keys.has(key)) {
this._keys.set(key, Math.random().toString());
}
return this._keys.get(key)!;
}
protected async _handleAddCard() {
this._selectedCard = this._config!.cards.length;
}
protected _handleSelectedCard(ev) {
this._GUImode = true;
this._guiModeAvailable = true;
this._selectedCard = parseInt(ev.detail.name, 10);
}
protected _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) {
protected _cardsChanged(ev: HASSDomEvent<CardsChangedEvent>) {
ev.stopPropagation();
if (!this._config) {
return;
}
const cards = [...this._config.cards];
const newCard = ev.detail.config as LovelaceCardConfig;
cards[this._selectedCard] = newCard;
this._config = { ...this._config, cards };
this._guiModeAvailable = ev.detail.guiModeAvailable;
fireEvent(this, "config-changed", { config: this._config });
}
protected _handleCardPicked(ev) {
ev.stopPropagation();
if (!this._config) {
return;
}
const config = ev.detail.config;
const cards = [...this._config.cards, config];
this._config = { ...this._config, cards };
this._keys.clear();
fireEvent(this, "config-changed", { config: this._config });
}
protected _handleCopyCard() {
if (!this._config) {
return;
}
this._clipboard = deepClone(this._config.cards[this._selectedCard]);
}
protected _handleCutCard() {
this._handleCopyCard();
this._handleDeleteCard();
}
protected _handleDeleteCard() {
if (!this._config) {
return;
}
const cards = [...this._config.cards];
cards.splice(this._selectedCard, 1);
this._config = { ...this._config, cards };
this._selectedCard = Math.max(0, this._selectedCard - 1);
this._keys.clear();
fireEvent(this, "config-changed", { config: this._config });
}
protected _handleMove(ev: Event) {
if (!this._config) {
return;
}
const move = (ev.currentTarget as any).move;
const source = this._selectedCard;
const target = source + move;
const cards = [...this._config.cards];
const card = cards.splice(this._selectedCard, 1)[0];
cards.splice(target, 0, card);
this._config = {
...this._config,
cards,
cards: ev.detail.cards,
};
this._selectedCard = target;
this._keys.clear();
fireEvent(this, "config-changed", { config: this._config });
}
protected _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
ev.stopPropagation();
this._GUImode = ev.detail.guiMode;
this._guiModeAvailable = ev.detail.guiModeAvailable;
}
protected _toggleMode(): void {
this._cardEditorEl?.toggleMode();
fireEvent(this, "config-changed", {
config: this._config,
guiModeAvailable: ev.detail.guiModeAvailable,
});
}
protected _valueChanged(ev: CustomEvent): void {
@@ -329,45 +117,7 @@ export class HuiStackCardEditor
`ui.panel.lovelace.editor.card.${this._config!.type}.${schema.name}`
);
static get styles(): CSSResultGroup {
return [
configElementStyle,
css`
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-tab-group {
flex-grow: 1;
min-width: 0;
--ha-tab-track-color: var(--card-background-color);
}
#card-options {
display: flex;
justify-content: flex-end;
width: 100%;
}
#editor {
border: 1px solid var(--divider-color);
padding: 12px;
}
@media (max-width: 450px) {
#editor {
margin: 0 -12px;
}
}
.gui-mode-button {
margin-right: auto;
margin-inline-end: auto;
margin-inline-start: initial;
}
`,
];
}
static styles = configElementStyle;
}
declare global {
@@ -1,5 +1,6 @@
import {
array,
any,
boolean,
dynamic,
enums,
@@ -61,6 +62,14 @@ const actionConfigStructAssist = type({
start_listening: optional(boolean()),
});
const actionConfigStructShowPopup = object({
action: literal("show-popup"),
desktop_mode: optional(enums(["popover", "dialog"])),
mobile_mode: optional(enums(["bottom-sheet", "dialog"])),
cards: array(any()),
confirmation: optional(actionConfigStructConfirmation),
});
const actionConfigStructMoreInfo = type({
action: literal("more-info"),
entity: optional(string()),
@@ -76,6 +85,7 @@ export const actionConfigStructType = object({
"url",
"navigate",
"assist",
"show-popup",
]),
confirmation: optional(actionConfigStructConfirmation),
});
@@ -98,6 +108,9 @@ export const actionConfigStruct = dynamic<any>((value) => {
case "assist": {
return actionConfigStructAssist;
}
case "show-popup": {
return actionConfigStructShowPopup;
}
case "more-info": {
return actionConfigStructMoreInfo;
}
+3 -1
View File
@@ -8496,7 +8496,8 @@
"no_entity_toggle": "No entity provided to toggle",
"no_navigation_path": "No navigation path specified",
"no_url": "No URL to open specified",
"no_action": "No action to run specified"
"no_action": "No action to run specified",
"no_popup_cards": "No cards configured for the popup"
},
"entities": {
"never_triggered": "Never triggered"
@@ -9055,6 +9056,7 @@
"toggle": "Toggle",
"navigate": "Navigate",
"assist": "Assist",
"show-popup": "Show popup",
"url": "URL",
"none": "Nothing"
}