404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import {
|
|
mdiArrowAll,
|
|
mdiArrowDown,
|
|
mdiArrowUp,
|
|
mdiDelete,
|
|
mdiPencil,
|
|
mdiViewGridPlus,
|
|
} from "@mdi/js";
|
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
|
import { customElement, property, state } from "lit/decorators";
|
|
import { repeat } from "lit/directives/repeat";
|
|
import { styleMap } from "lit/directives/style-map";
|
|
import "../../../components/ha-icon-button";
|
|
import "../../../components/ha-sortable";
|
|
import "../../../components/ha-svg-icon";
|
|
import type { LovelaceViewElement } from "../../../data/lovelace";
|
|
import { LovelaceSectionConfig as LovelaceRawSectionConfig } from "../../../data/lovelace/config/section";
|
|
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
|
import {
|
|
showConfirmationDialog,
|
|
showPromptDialog,
|
|
} from "../../../dialogs/generic/show-dialog-box";
|
|
import type { HomeAssistant } from "../../../types";
|
|
import { addSection, deleteSection, moveSection } from "../editor/config-util";
|
|
import {
|
|
findLovelaceContainer,
|
|
updateLovelaceContainer,
|
|
} from "../editor/lovelace-path";
|
|
import { HuiSection } from "../sections/hui-section";
|
|
import type { Lovelace, LovelaceBadge } from "../types";
|
|
import { isTouch } from "../../../util/is_touch";
|
|
import { listenMediaQuery } from "../../../common/dom/media_query";
|
|
|
|
@customElement("hui-sections-view")
|
|
export class SectionsView extends LitElement implements LovelaceViewElement {
|
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
|
|
@property({ attribute: false }) public lovelace?: Lovelace;
|
|
|
|
@property({ type: Number }) public index?: number;
|
|
|
|
@property({ type: Boolean }) public isStrategy = false;
|
|
|
|
@property({ attribute: false }) public sections: HuiSection[] = [];
|
|
|
|
@property({ attribute: false }) public badges: LovelaceBadge[] = [];
|
|
|
|
@state() private _config?: LovelaceViewConfig;
|
|
|
|
@state() private _narrow = false;
|
|
|
|
private _unsubMql?: () => void;
|
|
|
|
public connectedCallback() {
|
|
super.connectedCallback();
|
|
this._unsubMql = listenMediaQuery("(max-width: 600px)", (matches) => {
|
|
this._narrow = matches;
|
|
});
|
|
}
|
|
|
|
public disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
this._unsubMql?.();
|
|
this._unsubMql = undefined;
|
|
}
|
|
|
|
public setConfig(config: LovelaceViewConfig): void {
|
|
this._config = config;
|
|
}
|
|
|
|
private _sectionConfigKeys = new WeakMap<LovelaceRawSectionConfig, string>();
|
|
|
|
private _getKey(sectionConfig: LovelaceRawSectionConfig) {
|
|
if (!this._sectionConfigKeys.has(sectionConfig)) {
|
|
this._sectionConfigKeys.set(sectionConfig, Math.random().toString());
|
|
}
|
|
return this._sectionConfigKeys.get(sectionConfig)!;
|
|
}
|
|
|
|
protected render() {
|
|
if (!this.lovelace) return nothing;
|
|
|
|
const sectionsConfig = this._config?.sections ?? [];
|
|
|
|
const editMode = this.lovelace.editMode;
|
|
|
|
const supportDnD = !(isTouch && this._narrow);
|
|
|
|
return html`
|
|
${this.badges.length > 0
|
|
? html`<div class="badges">${this.badges}</div>`
|
|
: ""}
|
|
<ha-sortable
|
|
.disabled=${!editMode && !supportDnD}
|
|
@item-moved=${this._sectionMoved}
|
|
group="section"
|
|
handle-selector=".handle"
|
|
draggable-selector=".section"
|
|
.rollback=${false}
|
|
>
|
|
<div
|
|
class="container"
|
|
style=${styleMap({
|
|
"--section-count": String(
|
|
sectionsConfig.length + (editMode ? 1 : 0)
|
|
),
|
|
})}
|
|
>
|
|
${repeat(
|
|
sectionsConfig,
|
|
(sectionConfig) => this._getKey(sectionConfig),
|
|
(_sectionConfig, idx) => {
|
|
const section = this.sections[idx];
|
|
(section as any).itemPath = [idx];
|
|
return html`
|
|
<div class="section">
|
|
${editMode
|
|
? html`
|
|
<div class="section-overlay">
|
|
<div class="section-actions">
|
|
${supportDnD
|
|
? html`
|
|
<ha-svg-icon
|
|
aria-hidden="true"
|
|
class="handle"
|
|
.path=${mdiArrowAll}
|
|
></ha-svg-icon>
|
|
`
|
|
: html`
|
|
<ha-icon-button
|
|
.label=${"Down"}
|
|
@click=${this._moveDown}
|
|
.index=${idx}
|
|
.path=${mdiArrowDown}
|
|
></ha-icon-button>
|
|
<ha-icon-button
|
|
.label=${"Up"}
|
|
@click=${this._moveUp}
|
|
.index=${idx}
|
|
.path=${mdiArrowUp}
|
|
></ha-icon-button>
|
|
`}
|
|
<ha-icon-button
|
|
.label=${this.hass.localize("ui.common.edit")}
|
|
@click=${this._editSection}
|
|
.index=${idx}
|
|
.path=${mdiPencil}
|
|
></ha-icon-button>
|
|
<ha-icon-button
|
|
.label=${this.hass.localize("ui.common.delete")}
|
|
@click=${this._deleteSection}
|
|
.index=${idx}
|
|
.path=${mdiDelete}
|
|
></ha-icon-button>
|
|
</div>
|
|
</div>
|
|
`
|
|
: nothing}
|
|
<div class="section-wrapper">${section}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
)}
|
|
${editMode
|
|
? html`
|
|
<button
|
|
class="add"
|
|
@click=${this._addSection}
|
|
aria-label=${this.hass.localize(
|
|
"ui.panel.lovelace.editor.section.add_section"
|
|
)}
|
|
.title=${this.hass.localize(
|
|
"ui.panel.lovelace.editor.section.add_section"
|
|
)}
|
|
>
|
|
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
|
|
</button>
|
|
`
|
|
: nothing}
|
|
</div>
|
|
</ha-sortable>
|
|
`;
|
|
}
|
|
|
|
private _addSection(): void {
|
|
const newConfig = addSection(this.lovelace!.config, this.index!, {
|
|
type: "grid",
|
|
cards: [],
|
|
});
|
|
this.lovelace!.saveConfig(newConfig);
|
|
}
|
|
|
|
private async _editSection(ev) {
|
|
const index = ev.currentTarget.index;
|
|
|
|
const path = [this.index!, index] as [number, number];
|
|
|
|
const section = findLovelaceContainer(
|
|
this.lovelace!.config,
|
|
path
|
|
) as LovelaceRawSectionConfig;
|
|
|
|
const newTitle = !section.title;
|
|
|
|
const title = await showPromptDialog(this, {
|
|
title: this.hass.localize(
|
|
`ui.panel.lovelace.editor.edit_section_title.${newTitle ? "title_new" : "title"}`
|
|
),
|
|
inputLabel: this.hass.localize(
|
|
"ui.panel.lovelace.editor.edit_section_title.input_label"
|
|
),
|
|
inputType: "string",
|
|
defaultValue: section.title,
|
|
confirmText: newTitle
|
|
? this.hass.localize("ui.common.add")
|
|
: this.hass.localize("ui.common.save"),
|
|
});
|
|
|
|
if (title === null) {
|
|
return;
|
|
}
|
|
|
|
const newConfig = updateLovelaceContainer(this.lovelace!.config, path, {
|
|
...section,
|
|
title: title || undefined,
|
|
});
|
|
|
|
this.lovelace!.saveConfig(newConfig);
|
|
}
|
|
|
|
private async _deleteSection(ev) {
|
|
const index = ev.currentTarget.index;
|
|
|
|
const path = [this.index!, index] as [number, number];
|
|
|
|
const section = findLovelaceContainer(
|
|
this.lovelace!.config,
|
|
path
|
|
) as LovelaceRawSectionConfig;
|
|
|
|
const title = section.title?.trim();
|
|
const cardCount = section.cards?.length;
|
|
|
|
if (title || cardCount) {
|
|
const named = title ? "named" : "unnamed";
|
|
const type = cardCount ? "cards" : "only";
|
|
|
|
const confirm = await showConfirmationDialog(this, {
|
|
title: this.hass.localize(
|
|
"ui.panel.lovelace.editor.delete_section.title"
|
|
),
|
|
text: this.hass.localize(
|
|
`ui.panel.lovelace.editor.delete_section.text_${named}_section_${type}`,
|
|
{ name: title }
|
|
),
|
|
confirmText: this.hass.localize("ui.common.delete"),
|
|
destructive: true,
|
|
});
|
|
|
|
if (!confirm) return;
|
|
}
|
|
|
|
const newConfig = deleteSection(this.lovelace!.config, this.index!, index);
|
|
this.lovelace!.saveConfig(newConfig);
|
|
}
|
|
|
|
private _sectionMoved(ev: CustomEvent) {
|
|
ev.stopPropagation();
|
|
const { oldIndex, newIndex } = ev.detail;
|
|
|
|
const newConfig = moveSection(
|
|
this.lovelace!.config,
|
|
[this.index!, oldIndex],
|
|
[this.index!, newIndex]
|
|
);
|
|
this.lovelace!.saveConfig(newConfig);
|
|
}
|
|
|
|
private _moveDown(ev) {
|
|
ev.stopPropagation();
|
|
const { index } = ev.currentTarget;
|
|
|
|
const newConfig = moveSection(
|
|
this.lovelace!.config,
|
|
[this.index!, index],
|
|
[this.index!, index + 1]
|
|
);
|
|
this.lovelace!.saveConfig(newConfig);
|
|
}
|
|
|
|
private _moveUp(ev) {
|
|
ev.stopPropagation();
|
|
const { index } = ev.currentTarget;
|
|
|
|
const newConfig = moveSection(
|
|
this.lovelace!.config,
|
|
[this.index!, index],
|
|
[this.index!, index - 1]
|
|
);
|
|
this.lovelace!.saveConfig(newConfig);
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return css`
|
|
:host {
|
|
display: block;
|
|
}
|
|
|
|
.badges {
|
|
margin: 12px 8px 16px 8px;
|
|
font-size: 85%;
|
|
text-align: center;
|
|
}
|
|
|
|
.section {
|
|
position: relative;
|
|
border-radius: var(--ha-card-border-radius, 12px);
|
|
}
|
|
|
|
.container {
|
|
/* Inputs */
|
|
--grid-gap: 20px;
|
|
--grid-max-section-count: 4;
|
|
--grid-section-min-width: 320px;
|
|
|
|
/* Calculated */
|
|
--max-count: min(var(--section-count), var(--grid-max-section-count));
|
|
--grid-max-width: calc(
|
|
(var(--max-count) + 1) * var(--grid-section-min-width) +
|
|
(var(--max-count) + 2) * var(--grid-gap) - 1px
|
|
);
|
|
|
|
display: grid;
|
|
grid-template-columns: repeat(
|
|
auto-fit,
|
|
minmax(var(--grid-section-min-width), 1fr)
|
|
);
|
|
grid-gap: 8px var(--grid-gap);
|
|
justify-content: center;
|
|
padding: var(--grid-gap);
|
|
box-sizing: border-box;
|
|
max-width: var(--grid-max-width);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.container {
|
|
grid-template-columns: 1fr;
|
|
--grid-gap: 8px;
|
|
}
|
|
}
|
|
|
|
.section-actions {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 0;
|
|
opacity: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: opacity 0.2s ease-in-out;
|
|
background-color: rgba(var(--rgb-card-background-color), 0.3);
|
|
border-radius: 18px;
|
|
background: var(--secondary-background-color);
|
|
--mdc-icon-button-size: 36px;
|
|
--mdc-icon-size: 20px;
|
|
color: var(--primary-text-color);
|
|
}
|
|
|
|
.handle {
|
|
cursor: grab;
|
|
padding: 8px;
|
|
}
|
|
|
|
.add {
|
|
margin-top: calc(66px + 8px);
|
|
outline: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
border-radius: var(--ha-card-border-radius, 12px);
|
|
border: 2px dashed var(--primary-color);
|
|
order: 1;
|
|
height: 66px;
|
|
padding: 8px;
|
|
box-sizing: content-box;
|
|
}
|
|
|
|
.add:focus {
|
|
border: 2px solid var(--primary-color);
|
|
}
|
|
|
|
.sortable-ghost {
|
|
border-radius: var(--ha-card-border-radius, 12px);
|
|
}
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"hui-sections-view": SectionsView;
|
|
}
|
|
}
|