ha-frontend/src/panels/lovelace/components/hui-card-options.ts

391 lines
12 KiB
TypeScript

import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiFileMoveOutline,
mdiMinus,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
LitElement,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, queryAssignedNodes } from "lit/decorators";
import { storage } from "../../../common/decorators/storage";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button-menu";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { saveConfig } from "../../../data/lovelace/config/types";
import {
showAlertDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
import { computeCardSize } from "../common/compute-card-size";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import {
addCard,
deleteCard,
moveCardToContainer,
moveCardToIndex,
} from "../editor/config-util";
import {
LovelaceCardPath,
findLovelaceCards,
getLovelaceContainerPath,
parseLovelaceCardPath,
} from "../editor/lovelace-path";
import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog";
import { Lovelace, LovelaceCard } from "../types";
@customElement("hui-card-options")
export class HuiCardOptions extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property({ type: Array }) public path?: LovelaceCardPath;
@queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;
@property({ type: Boolean }) public hidePosition = false;
@storage({
key: "lovelaceClipboard",
state: false,
subscribe: false,
storage: "sessionStorage",
})
protected _clipboard?: LovelaceCardConfig;
public getCardSize() {
return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1;
}
protected updated(changedProps: PropertyValues) {
if (!changedProps.has("path") || !this.path) {
return;
}
const { viewIndex } = parseLovelaceCardPath(this.path);
this.classList.toggle(
"panel",
this.lovelace!.config.views[viewIndex].panel
);
}
private get _cards() {
const containerPath = getLovelaceContainerPath(this.path!);
return findLovelaceCards(this.lovelace!.config, containerPath)!;
}
protected render(): TemplateResult {
const { cardIndex } = parseLovelaceCardPath(this.path!);
return html`
<div class="card"><slot></slot></div>
<ha-card>
<div class="card-actions">
<mwc-button @click=${this._editCard}
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.edit"
)}</mwc-button
>
<div class="right">
<slot name="buttons"></slot>
${!this.hidePosition
? html`
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.decrease_position"
)}
.path=${mdiMinus}
class="move-arrow"
@click=${this._decreaseCardPosiion}
?disabled=${cardIndex === 0}
></ha-icon-button>
<ha-icon-button
@click=${this._changeCardPosition}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.change_position"
)}
>
<div class="position-badge">${cardIndex + 1}</div>
</ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.increase_position"
)}
.path=${mdiPlus}
class="move-arrow"
@click=${this._increaseCardPosition}
.disabled=${this._cards!.length === cardIndex + 1}
></ha-icon-button>
`
: nothing}
<ha-button-menu @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.options"
)}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiFileMoveOutline}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.move"
)}
</ha-list-item>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.duplicate"
)}
</ha-list-item>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiContentCopy}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
)}
</ha-list-item>
<ha-list-item graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiContentCut}
></ha-svg-icon>
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.cut")}
</ha-list-item>
<li divider role="separator"></li>
<ha-list-item class="warning" graphic="icon">
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete"
)}
</ha-list-item>
</ha-button-menu>
</div>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host(:hover) {
outline: 2px solid var(--primary-color);
}
:host(:not(.panel)) ::slotted(*) {
display: block;
}
:host(.panel) .card {
height: calc(100% - 59px);
}
ha-card {
border-top-right-radius: 0;
border-top-left-radius: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.right {
display: flex;
align-items: center;
}
.position-badge {
display: block;
width: 24px;
line-height: 24px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 500;
text-align: center;
font-size: 14px;
background-color: var(--app-header-edit-background-color, #455a64);
color: var(--app-header-edit-text-color, white);
}
ha-icon-button {
color: var(--primary-text-color);
}
ha-icon-button.move-arrow[disabled] {
color: var(--disabled-text-color);
}
ha-list-item {
cursor: pointer;
white-space: nowrap;
}
`,
];
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._moveCard();
break;
case 1:
this._duplicateCard();
break;
case 2:
this._copyCard();
break;
case 3:
this._cutCard();
break;
case 4:
this._deleteCard(true);
break;
}
}
private _duplicateCard(): void {
const { cardIndex } = parseLovelaceCardPath(this.path!);
const containerPath = getLovelaceContainerPath(this.path!);
const cardConfig = this._cards![cardIndex];
showEditCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
path: containerPath,
cardConfig,
});
}
private _editCard(): void {
fireEvent(this, "ll-edit-card", { path: this.path! });
}
private _cutCard(): void {
this._copyCard();
this._deleteCard(false);
}
private _copyCard(): void {
const { cardIndex } = parseLovelaceCardPath(this.path!);
const cardConfig = this._cards[cardIndex];
this._clipboard = deepClone(cardConfig);
}
private _decreaseCardPosiion(): void {
const lovelace = this.lovelace!;
const path = this.path!;
const { cardIndex } = parseLovelaceCardPath(path);
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex - 1));
}
private _increaseCardPosition(): void {
const lovelace = this.lovelace!;
const path = this.path!;
const { cardIndex } = parseLovelaceCardPath(path);
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex + 1));
}
private async _changeCardPosition(): Promise<void> {
const lovelace = this.lovelace!;
const path = this.path!;
const { cardIndex } = parseLovelaceCardPath(path);
const positionString = await showPromptDialog(this, {
title: this.hass!.localize(
"ui.panel.lovelace.editor.change_position.title"
),
text: this.hass!.localize(
"ui.panel.lovelace.editor.change_position.text"
),
inputType: "number",
inputMin: "1",
placeholder: String(cardIndex + 1),
});
if (!positionString) return;
const position = parseInt(positionString);
if (isNaN(position)) return;
const newIndex = position - 1;
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, newIndex));
}
private _moveCard(): void {
showSelectViewDialog(this, {
lovelaceConfig: this.lovelace!.config,
urlPath: this.lovelace!.urlPath,
allowDashboardChange: true,
header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"),
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
if (urlPath === this.lovelace!.urlPath) {
this.lovelace!.saveConfig(
moveCardToContainer(this.lovelace!.config, this.path!, [viewIndex])
);
showSaveSuccessToast(this, this.hass!);
return;
}
try {
const { cardIndex } = parseLovelaceCardPath(this.path!);
await saveConfig(
this.hass!,
urlPath,
addCard(selectedDashConfig, [viewIndex], this._cards[cardIndex])
);
this.lovelace!.saveConfig(
deleteCard(this.lovelace!.config, this.path!)
);
showSaveSuccessToast(this, this.hass!);
} catch (err: any) {
showAlertDialog(this, {
text: `Moving failed: ${err.message}`,
});
}
},
});
}
private _deleteCard(confirm: boolean): void {
fireEvent(this, "ll-delete-card", { path: this.path!, confirm });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-card-options": HuiCardOptions;
}
}