ha-frontend/src/panels/lovelace/sections/hui-grid-section.ts

247 lines
7.2 KiB
TypeScript

import { mdiPlus } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HaSortableOptions } from "../../../components/ha-sortable";
import { LovelaceSectionElement } from "../../../data/lovelace";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { HuiErrorCard } from "../cards/hui-error-card";
import "../components/hui-card-edit-mode";
import { moveCard } from "../editor/config-util";
import type { Lovelace, LovelaceCard } from "../types";
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delay: 200,
delayOnTouchOnly: true,
direction: "vertical",
invertedSwapThreshold: 0.7,
} as HaSortableOptions;
export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property({ type: Number }) public index?: number;
@property({ type: Number }) public viewIndex?: number;
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@state() _config?: LovelaceSectionConfig;
@state() _dragging = false;
public setConfig(config: LovelaceSectionConfig): void {
this._config = config;
}
private _cardConfigKeys = new WeakMap<LovelaceCardConfig, string>();
private _getKey(cardConfig: LovelaceCardConfig) {
if (!this._cardConfigKeys.has(cardConfig)) {
this._cardConfigKeys.set(cardConfig, Math.random().toString());
}
return this._cardConfigKeys.get(cardConfig)!;
}
render() {
if (!this.cards || !this._config) return nothing;
const cardsConfig = this._config?.cards ?? [];
const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy);
return html`
${this._config.title || this.lovelace?.editMode
? html`
<h2
class="title ${classMap({
placeholder: !this._config.title,
})}"
>
${this._config.title ||
this.hass.localize(
"ui.panel.lovelace.editor.section.unnamed_section"
)}
</h2>
`
: nothing}
<ha-sortable
.disabled=${!editMode}
@item-moved=${this._cardMoved}
@drag-start=${this._dragStart}
@drag-end=${this._dragEnd}
group="card"
draggable-selector=".card"
.path=${[this.viewIndex, this.index]}
.rollback=${false}
.options=${CARD_SORTABLE_OPTIONS}
invert-swap
>
<div class="container ${classMap({ "edit-mode": editMode })}">
${repeat(
cardsConfig,
(cardConfig) => this._getKey(cardConfig),
(_cardConfig, idx) => {
const card = this.cards![idx];
(card as any).editMode = editMode;
const size = card && (card as any).getGridSize?.();
return html`
<div
class="card"
style=${styleMap({
"--column-size": size?.[0],
"--row-size": size?.[1],
})}
>
${editMode
? html`
<hui-card-edit-mode
.hass=${this.hass}
.lovelace=${this.lovelace}
.path=${[this.viewIndex, this.index, idx]}
.hiddenOverlay=${this._dragging}
>
${card}
</hui-card-edit-mode>
`
: card}
</div>
`;
}
)}
${editMode
? html`
<button
class="add"
@click=${this._addCard}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.add_card"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.add_card"
)}
>
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
</button>
`
: nothing}
</div>
</ha-sortable>
`;
}
private _cardMoved(ev) {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const newConfig = moveCard(
this.lovelace!.config,
[...oldPath, oldIndex] as [number, number, number],
[...newPath, newIndex] as [number, number, number]
);
this.lovelace!.saveConfig(newConfig);
}
private _dragStart() {
this._dragging = true;
}
private _dragEnd() {
this._dragging = false;
}
private _addCard() {
fireEvent(this, "ll-create-card", { suggested: ["tile"] });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: flex;
flex-direction: column;
gap: 8px;
}
.container {
--column-count: 4;
display: grid;
grid-template-columns: repeat(var(--column-count), minmax(0, 1fr));
grid-auto-rows: minmax(66px, auto);
gap: 8px;
padding: 0;
margin: 0 auto;
}
.container.edit-mode {
padding: 8px;
border-radius: var(--ha-card-border-radius, 12px);
border: 2px dashed var(--divider-color);
min-height: 66px;
}
.title {
color: var(--primary-text-color);
font-size: 20px;
font-weight: normal;
margin: 0px;
letter-spacing: 0.1px;
line-height: 32px;
min-height: 32px;
display: block;
padding: 24px 10px 10px;
}
.title.placeholder {
color: var(--secondary-text-color);
font-style: italic;
}
.card {
border-radius: var(--ha-card-border-radius, 12px);
position: relative;
grid-row: span var(--row-size, 1);
grid-column: span var(--column-size, 4);
}
.add {
outline: none;
grid-row: span var(--row-size, 1);
grid-column: span var(--column-size, 2);
background: none;
cursor: pointer;
border-radius: var(--ha-card-border-radius, 12px);
border: 2px dashed var(--primary-color);
height: 66px;
order: 1;
}
.add:focus {
border-style: solid;
}
.sortable-ghost {
border-radius: var(--ha-card-border-radius, 12px);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-grid-section": GridSection;
}
}
customElements.define("hui-grid-section", GridSection);