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

304 lines
8.4 KiB
TypeScript

import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiPencil,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
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 "../../../components/ha-svg-icon";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import {
LovelaceCardPath,
findLovelaceCards,
getLovelaceContainerPath,
parseLovelaceCardPath,
} from "../editor/lovelace-path";
import { Lovelace } from "../types";
@customElement("hui-card-edit-mode")
export class HuiCardEditMode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace!: Lovelace;
@property({ type: Array }) public path!: LovelaceCardPath;
@property({ type: Boolean }) public hiddenOverlay = false;
@state()
public _menuOpened: boolean = false;
@state()
public _hover: boolean = false;
@state()
public _focused: boolean = false;
@storage({
key: "lovelaceClipboard",
state: false,
subscribe: false,
storage: "sessionStorage",
})
protected _clipboard?: LovelaceCardConfig;
private get _cards() {
const containerPath = getLovelaceContainerPath(this.path!);
return findLovelaceCards(this.lovelace!.config, containerPath)!;
}
private _touchStarted = false;
protected firstUpdated(): void {
this.addEventListener("focus", () => {
this._focused = true;
});
this.addEventListener("blur", () => {
this._focused = false;
});
this.addEventListener("touchstart", () => {
this._touchStarted = true;
});
this.addEventListener("touchend", () => {
setTimeout(() => {
this._touchStarted = false;
}, 10);
});
this.addEventListener("mouseenter", () => {
if (this._touchStarted) return;
this._hover = true;
});
this.addEventListener("mouseout", () => {
this._hover = false;
});
this.addEventListener("click", () => {
this._hover = true;
document.addEventListener("click", this._documentClicked);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
document.removeEventListener("click", this._documentClicked);
}
_documentClicked = (ev) => {
this._hover = ev.composedPath().includes(this);
document.removeEventListener("click", this._documentClicked);
};
protected render(): TemplateResult {
const showOverlay =
(this._hover || this._menuOpened || this._focused) && !this.hiddenOverlay;
return html`
<div class="card-wrapper" inert><slot></slot></div>
<div class="card-overlay ${classMap({ visible: showOverlay })}">
<div
class="edit"
@click=${this._editCard}
@keydown=${this._editCard}
tabindex="0"
>
<div class="edit-overlay"></div>
<ha-svg-icon class="edit" .path=${mdiPencil}> </ha-svg-icon>
</div>
<ha-button-menu
class="more"
corner="BOTTOM_END"
menuCorner="END"
.path=${[this.path!]}
@action=${this._handleAction}
@opened=${this._handleOpened}
@closed=${this._handleClosed}
>
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button>
<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 graphic="icon" class="warning">
${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
</div>
`;
}
private _handleOpened() {
this._menuOpened = true;
}
private _handleClosed() {
this._menuOpened = false;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._duplicateCard();
break;
case 1:
this._copyCard();
break;
case 2:
this._cutCard();
break;
case 3:
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(ev): void {
if (ev.defaultPrevented) {
return;
}
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.preventDefault();
ev.stopPropagation();
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 _deleteCard(confirm: boolean): void {
fireEvent(this, "ll-delete-card", { path: this.path!, confirm });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.card-overlay {
position: absolute;
opacity: 0;
pointer-events: none;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 180ms ease-in-out;
}
.card-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.card-wrapper {
position: relative;
height: 100%;
z-index: 0;
}
.edit {
outline: none !important;
cursor: pointer;
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--ha-card-border-radius, 12px);
z-index: 0;
}
.edit-overlay {
position: absolute;
inset: 0;
opacity: 0.8;
background-color: var(--primary-background-color);
border: 1px solid var(--divider-color);
border-radius: var(--ha-card-border-radius, 12px);
z-index: 0;
}
.edit ha-svg-icon {
display: flex;
position: relative;
color: var(--primary-text-color);
border-radius: 50%;
padding: 12px;
background: var(--secondary-background-color);
--mdc-icon-size: 24px;
}
.more {
position: absolute;
right: -6px;
top: -6px;
}
.more ha-icon-button {
cursor: pointer;
border-radius: 50%;
background: var(--secondary-background-color);
--mdc-icon-button-size: 32px;
--mdc-icon-size: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-card-edit-mode": HuiCardEditMode;
}
}