mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-16 14:17:16 +00:00
Compare commits
10 Commits
dev
...
ll-popup-action
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e5605e6c0 | |||
| 346d41fde2 | |||
| cc251de994 | |||
| a38507a386 | |||
| 541ce87e8a | |||
| 8502b1f87e | |||
| 123ecc52f8 | |||
| 6ef70def5d | |||
| 98f0c977b6 | |||
| 651192dc49 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user