mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add visibility option to sections (conditional section) (#20805)
* Add first version of section visibility option * Move visibility logic into view * Simplify section view structure * Don't add hidden section to dom * Move visilibity logic to hui-section * Setup section editor * Add visibility view * Add basic settings editor * Improve visibility editor * Update conditional base * Feedbacks * Better typings
This commit is contained in:
parent
4cc5d2d04b
commit
a2a89502d8
@ -1,3 +1,5 @@
|
|||||||
|
export type MediaQueriesListener = () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a media query. Listener is called right away and when it matches.
|
* Attach a media query. Listener is called right away and when it matches.
|
||||||
* @param mediaQuery media query to match.
|
* @param mediaQuery media query to match.
|
||||||
@ -7,7 +9,7 @@
|
|||||||
export const listenMediaQuery = (
|
export const listenMediaQuery = (
|
||||||
mediaQuery: string,
|
mediaQuery: string,
|
||||||
matchesChanged: (matches: boolean) => void
|
matchesChanged: (matches: boolean) => void
|
||||||
) => {
|
): MediaQueriesListener => {
|
||||||
const mql = matchMedia(mediaQuery);
|
const mql = matchMedia(mediaQuery);
|
||||||
const listener = (e) => matchesChanged(e.matches);
|
const listener = (e) => matchesChanged(e.matches);
|
||||||
mql.addListener(listener);
|
mql.addListener(listener);
|
||||||
|
@ -313,31 +313,38 @@ export class HaChartBase extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _loading = false;
|
||||||
|
|
||||||
private async _setupChart() {
|
private async _setupChart() {
|
||||||
|
if (this._loading) return;
|
||||||
const ctx: CanvasRenderingContext2D = this.renderRoot
|
const ctx: CanvasRenderingContext2D = this.renderRoot
|
||||||
.querySelector("canvas")!
|
.querySelector("canvas")!
|
||||||
.getContext("2d")!;
|
.getContext("2d")!;
|
||||||
|
this._loading = true;
|
||||||
|
try {
|
||||||
|
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
|
||||||
|
|
||||||
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
|
const computedStyles = getComputedStyle(this);
|
||||||
|
|
||||||
const computedStyles = getComputedStyle(this);
|
ChartConstructor.defaults.borderColor =
|
||||||
|
computedStyles.getPropertyValue("--divider-color");
|
||||||
|
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
|
||||||
|
"--secondary-text-color"
|
||||||
|
);
|
||||||
|
ChartConstructor.defaults.font.family =
|
||||||
|
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
|
||||||
|
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
|
||||||
|
"Roboto, Noto, sans-serif";
|
||||||
|
|
||||||
ChartConstructor.defaults.borderColor =
|
this.chart = new ChartConstructor(ctx, {
|
||||||
computedStyles.getPropertyValue("--divider-color");
|
type: this.chartType,
|
||||||
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
|
data: this.data,
|
||||||
"--secondary-text-color"
|
options: this._createOptions(),
|
||||||
);
|
plugins: this._createPlugins(),
|
||||||
ChartConstructor.defaults.font.family =
|
});
|
||||||
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
|
} finally {
|
||||||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
|
this._loading = false;
|
||||||
"Roboto, Noto, sans-serif";
|
}
|
||||||
|
|
||||||
this.chart = new ChartConstructor(ctx, {
|
|
||||||
type: this.chartType,
|
|
||||||
data: this.data,
|
|
||||||
options: this._createOptions(),
|
|
||||||
plugins: this._createPlugins(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createOptions() {
|
private _createOptions() {
|
||||||
|
@ -178,16 +178,24 @@ export class HaMap extends ReactiveElement {
|
|||||||
map!.classList.toggle("forced-light", this.themeMode === "light");
|
map!.classList.toggle("forced-light", this.themeMode === "light");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _loading = false;
|
||||||
|
|
||||||
private async _loadMap(): Promise<void> {
|
private async _loadMap(): Promise<void> {
|
||||||
|
if (this._loading) return;
|
||||||
let map = this.shadowRoot!.getElementById("map");
|
let map = this.shadowRoot!.getElementById("map");
|
||||||
if (!map) {
|
if (!map) {
|
||||||
map = document.createElement("div");
|
map = document.createElement("div");
|
||||||
map.id = "map";
|
map.id = "map";
|
||||||
this.shadowRoot!.append(map);
|
this.shadowRoot!.append(map);
|
||||||
}
|
}
|
||||||
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
|
this._loading = true;
|
||||||
this._updateMapStyle();
|
try {
|
||||||
this._loaded = true;
|
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
|
||||||
|
this._updateMapStyle();
|
||||||
|
this._loaded = true;
|
||||||
|
} finally {
|
||||||
|
this._loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
|
||||||
import type { LovelaceCardConfig } from "./card";
|
import type { LovelaceCardConfig } from "./card";
|
||||||
import type { LovelaceStrategyConfig } from "./strategy";
|
import type { LovelaceStrategyConfig } from "./strategy";
|
||||||
|
|
||||||
export interface LovelaceBaseSectionConfig {
|
export interface LovelaceBaseSectionConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
visibility?: Condition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
|
import {
|
||||||
|
MediaQueriesListener,
|
||||||
|
listenMediaQuery,
|
||||||
|
} from "../../../common/dom/media_query";
|
||||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
import { UNAVAILABLE } from "../../../data/entity";
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
@ -308,3 +312,45 @@ export function addEntityToCondition(
|
|||||||
}
|
}
|
||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractMediaQueries(conditions: Condition[]): string[] {
|
||||||
|
return conditions.reduce<string[]>((array, c) => {
|
||||||
|
if ("conditions" in c && c.conditions) {
|
||||||
|
array.push(...extractMediaQueries(c.conditions));
|
||||||
|
}
|
||||||
|
if (c.condition === "screen" && c.media_query) {
|
||||||
|
array.push(c.media_query);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachConditionMediaQueriesListeners(
|
||||||
|
conditions: Condition[],
|
||||||
|
hass: HomeAssistant,
|
||||||
|
onChange: (visibility: boolean) => void
|
||||||
|
): MediaQueriesListener[] {
|
||||||
|
// For performance, if there is only one condition and it's a screen condition, set the visibility directly
|
||||||
|
if (
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
conditions[0].media_query
|
||||||
|
) {
|
||||||
|
const listener = listenMediaQuery(conditions[0].media_query, (matches) => {
|
||||||
|
onChange(matches);
|
||||||
|
});
|
||||||
|
return [listener];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQueries = extractMediaQueries(conditions);
|
||||||
|
|
||||||
|
const listeners = mediaQueries.map((query) => {
|
||||||
|
const listener = listenMediaQuery(query, () => {
|
||||||
|
const visibility = checkConditionsMet(conditions, hass);
|
||||||
|
onChange(visibility);
|
||||||
|
});
|
||||||
|
return listener;
|
||||||
|
});
|
||||||
|
|
||||||
|
return listeners;
|
||||||
|
}
|
||||||
|
@ -1,32 +1,19 @@
|
|||||||
import { PropertyValues, ReactiveElement } from "lit";
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
import { deepEqual } from "../../../common/util/deep-equal";
|
import { deepEqual } from "../../../common/util/deep-equal";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { ConditionalCardConfig } from "../cards/types";
|
import { ConditionalCardConfig } from "../cards/types";
|
||||||
import {
|
import {
|
||||||
Condition,
|
Condition,
|
||||||
LegacyCondition,
|
|
||||||
checkConditionsMet,
|
checkConditionsMet,
|
||||||
|
attachConditionMediaQueriesListeners,
|
||||||
|
extractMediaQueries,
|
||||||
validateConditionalConfig,
|
validateConditionalConfig,
|
||||||
} from "../common/validate-condition";
|
} from "../common/validate-condition";
|
||||||
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
|
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
|
||||||
import { LovelaceCard } from "../types";
|
import { LovelaceCard } from "../types";
|
||||||
|
|
||||||
function extractMediaQueries(
|
|
||||||
conditions: (Condition | LegacyCondition)[]
|
|
||||||
): string[] {
|
|
||||||
return conditions.reduce<string[]>((array, c) => {
|
|
||||||
if ("conditions" in c && c.conditions) {
|
|
||||||
array.push(...extractMediaQueries(c.conditions));
|
|
||||||
}
|
|
||||||
if ("condition" in c && c.condition === "screen" && c.media_query) {
|
|
||||||
array.push(c.media_query);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("hui-conditional-base")
|
@customElement("hui-conditional-base")
|
||||||
export class HuiConditionalBase extends ReactiveElement {
|
export class HuiConditionalBase extends ReactiveElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
@ -37,7 +24,7 @@ export class HuiConditionalBase extends ReactiveElement {
|
|||||||
|
|
||||||
protected _element?: LovelaceCard | LovelaceRow;
|
protected _element?: LovelaceCard | LovelaceRow;
|
||||||
|
|
||||||
private _mediaQueriesListeners: Array<() => void> = [];
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
private _mediaQueries: string[] = [];
|
private _mediaQueries: string[] = [];
|
||||||
|
|
||||||
@ -79,41 +66,31 @@ export class HuiConditionalBase extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _clearMediaQueries() {
|
private _clearMediaQueries() {
|
||||||
this._mediaQueries = [];
|
this._listeners.forEach((unsub) => unsub());
|
||||||
while (this._mediaQueriesListeners.length) {
|
this._listeners = [];
|
||||||
this._mediaQueriesListeners.pop()!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _listenMediaQueries() {
|
private _listenMediaQueries() {
|
||||||
if (!this._config) {
|
if (!this._config || !this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaQueries = extractMediaQueries(this._config.conditions);
|
const supportedConditions = this._config.conditions.filter(
|
||||||
|
(c) => "condition" in c
|
||||||
|
) as Condition[];
|
||||||
|
const mediaQueries = extractMediaQueries(supportedConditions);
|
||||||
|
|
||||||
if (deepEqual(mediaQueries, this._mediaQueries)) return;
|
if (deepEqual(mediaQueries, this._mediaQueries)) return;
|
||||||
|
|
||||||
this._mediaQueries = mediaQueries;
|
this._clearMediaQueries();
|
||||||
while (this._mediaQueriesListeners.length) {
|
|
||||||
this._mediaQueriesListeners.pop()!();
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaQueries.forEach((query) => {
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
const listener = listenMediaQuery(query, (matches) => {
|
supportedConditions,
|
||||||
// For performance, if there is only one condition and it's a screen condition, set the visibility directly
|
this.hass,
|
||||||
if (
|
(visibility) => {
|
||||||
this._config!.conditions.length === 1 &&
|
this._setVisibility(visibility);
|
||||||
"condition" in this._config!.conditions[0] &&
|
}
|
||||||
this._config!.conditions[0].condition === "screen"
|
);
|
||||||
) {
|
|
||||||
this._setVisibility(matches);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._updateVisibility();
|
|
||||||
});
|
|
||||||
this._mediaQueriesListeners.push(listener);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(changed: PropertyValues): void {
|
protected update(changed: PropertyValues): void {
|
||||||
|
@ -24,15 +24,15 @@ import {
|
|||||||
computeCards,
|
computeCards,
|
||||||
computeSection,
|
computeSection,
|
||||||
} from "../../common/generate-lovelace-config";
|
} from "../../common/generate-lovelace-config";
|
||||||
|
import {
|
||||||
|
findLovelaceContainer,
|
||||||
|
parseLovelaceContainerPath,
|
||||||
|
} from "../lovelace-path";
|
||||||
import "./hui-card-picker";
|
import "./hui-card-picker";
|
||||||
import "./hui-entity-picker-table";
|
import "./hui-entity-picker-table";
|
||||||
import { CreateCardDialogParams } from "./show-create-card-dialog";
|
import { CreateCardDialogParams } from "./show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "./show-edit-card-dialog";
|
import { showEditCardDialog } from "./show-edit-card-dialog";
|
||||||
import { showSuggestCardDialog } from "./show-suggest-card-dialog";
|
import { showSuggestCardDialog } from "./show-suggest-card-dialog";
|
||||||
import {
|
|
||||||
findLovelaceContainer,
|
|
||||||
parseLovelaceContainerPath,
|
|
||||||
} from "../lovelace-path";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -274,15 +274,17 @@ export class HuiCreateDialogCard
|
|||||||
|
|
||||||
let sectionOptions: Partial<LovelaceSectionConfig> = {};
|
let sectionOptions: Partial<LovelaceSectionConfig> = {};
|
||||||
|
|
||||||
const { sectionIndex } = parseLovelaceContainerPath(this._params!.path);
|
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(
|
||||||
|
this._params!.path
|
||||||
|
);
|
||||||
const isSection = sectionIndex !== undefined;
|
const isSection = sectionIndex !== undefined;
|
||||||
|
|
||||||
// If we are in a section, we want to keep the section options for the preview
|
// If we are in a section, we want to keep the section options for the preview
|
||||||
if (isSection) {
|
if (isSection) {
|
||||||
const containerConfig = findLovelaceContainer(
|
const containerConfig = findLovelaceContainer(
|
||||||
this._params!.lovelaceConfig!,
|
this._params!.lovelaceConfig!,
|
||||||
this._params!.path!
|
[viewIndex, sectionIndex]
|
||||||
) as LovelaceSectionConfig;
|
);
|
||||||
if (!isStrategySection(containerConfig)) {
|
if (!isStrategySection(containerConfig)) {
|
||||||
const { cards, title, ...rest } = containerConfig;
|
const { cards, title, ...rest } = containerConfig;
|
||||||
sectionOptions = rest;
|
sectionOptions = rest;
|
||||||
|
@ -1,16 +1,8 @@
|
|||||||
import { mdiPlus } from "@mdi/js";
|
import { mdiPlus } from "@mdi/js";
|
||||||
import {
|
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||||
CSSResultGroup,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
css,
|
|
||||||
html,
|
|
||||||
nothing,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
import "../../../../components/ha-alert";
|
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-list-item";
|
import "../../../../components/ha-list-item";
|
||||||
import type { HaSelect } from "../../../../components/ha-select";
|
import type { HaSelect } from "../../../../components/ha-select";
|
||||||
@ -21,12 +13,12 @@ import { Condition, LegacyCondition } from "../../common/validate-condition";
|
|||||||
import "./ha-card-condition-editor";
|
import "./ha-card-condition-editor";
|
||||||
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
|
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
|
||||||
import { LovelaceConditionEditorConstructor } from "./types";
|
import { LovelaceConditionEditorConstructor } from "./types";
|
||||||
|
import "./types/ha-card-condition-and";
|
||||||
import "./types/ha-card-condition-numeric_state";
|
import "./types/ha-card-condition-numeric_state";
|
||||||
|
import "./types/ha-card-condition-or";
|
||||||
import "./types/ha-card-condition-screen";
|
import "./types/ha-card-condition-screen";
|
||||||
import "./types/ha-card-condition-state";
|
import "./types/ha-card-condition-state";
|
||||||
import "./types/ha-card-condition-user";
|
import "./types/ha-card-condition-user";
|
||||||
import "./types/ha-card-condition-or";
|
|
||||||
import "./types/ha-card-condition-and";
|
|
||||||
|
|
||||||
const UI_CONDITION = [
|
const UI_CONDITION = [
|
||||||
"numeric_state",
|
"numeric_state",
|
||||||
@ -46,8 +38,6 @@ export class HaCardConditionsEditor extends LitElement {
|
|||||||
| LegacyCondition
|
| LegacyCondition
|
||||||
)[];
|
)[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public nested = false;
|
|
||||||
|
|
||||||
private _focusLastConditionOnChange = false;
|
private _focusLastConditionOnChange = false;
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
@ -83,15 +73,6 @@ export class HaCardConditionsEditor extends LitElement {
|
|||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="conditions">
|
<div class="conditions">
|
||||||
${!this.nested
|
|
||||||
? html`
|
|
||||||
<ha-alert alert-type="info">
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.condition-editor.explanation"
|
|
||||||
)}
|
|
||||||
</ha-alert>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this.conditions.map(
|
${this.conditions.map(
|
||||||
(cond, idx) => html`
|
(cond, idx) => html`
|
||||||
<ha-card-condition-editor
|
<ha-card-condition-editor
|
||||||
@ -172,9 +153,6 @@ export class HaCardConditionsEditor extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
mwc-tab-bar {
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
ha-alert {
|
ha-alert {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
@ -8,6 +8,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { any, array, assert, assign, object, optional } from "superstruct";
|
import { any, array, assert, assign, object, optional } from "superstruct";
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
|
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-list-item";
|
import "../../../../components/ha-list-item";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
@ -142,6 +143,11 @@ export class HuiConditionalCardEditor
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
|
<ha-alert alert-type="info">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.condition-editor.explanation"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
<ha-card-conditions-editor
|
<ha-card-conditions-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.conditions=${this._config.conditions}
|
.conditions=${this._config.conditions}
|
||||||
@ -231,6 +237,10 @@ export class HuiConditionalCardEditor
|
|||||||
mwc-tab-bar {
|
mwc-tab-bar {
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
|
ha-alert {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
.card {
|
.card {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
|
@ -226,7 +226,7 @@ export const addSection = (
|
|||||||
viewIndex: number,
|
viewIndex: number,
|
||||||
sectionConfig: LovelaceSectionRawConfig
|
sectionConfig: LovelaceSectionRawConfig
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const view = findLovelaceContainer(config, [viewIndex]) as LovelaceViewConfig;
|
const view = findLovelaceContainer(config, [viewIndex]);
|
||||||
if (isStrategyView(view)) {
|
if (isStrategyView(view)) {
|
||||||
throw new Error("Deleting sections in a strategy is not supported.");
|
throw new Error("Deleting sections in a strategy is not supported.");
|
||||||
}
|
}
|
||||||
@ -246,7 +246,7 @@ export const deleteSection = (
|
|||||||
viewIndex: number,
|
viewIndex: number,
|
||||||
sectionIndex: number
|
sectionIndex: number
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const view = findLovelaceContainer(config, [viewIndex]) as LovelaceViewConfig;
|
const view = findLovelaceContainer(config, [viewIndex]);
|
||||||
if (isStrategyView(view)) {
|
if (isStrategyView(view)) {
|
||||||
throw new Error("Deleting sections in a strategy is not supported.");
|
throw new Error("Deleting sections in a strategy is not supported.");
|
||||||
}
|
}
|
||||||
@ -267,7 +267,7 @@ export const insertSection = (
|
|||||||
sectionIndex: number,
|
sectionIndex: number,
|
||||||
sectionConfig: LovelaceSectionRawConfig
|
sectionConfig: LovelaceSectionRawConfig
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const view = findLovelaceContainer(config, [viewIndex]) as LovelaceViewConfig;
|
const view = findLovelaceContainer(config, [viewIndex]);
|
||||||
if (isStrategyView(view)) {
|
if (isStrategyView(view)) {
|
||||||
throw new Error("Inserting sections in a strategy is not supported.");
|
throw new Error("Inserting sections in a strategy is not supported.");
|
||||||
}
|
}
|
||||||
@ -291,10 +291,7 @@ export const moveSection = (
|
|||||||
fromPath: [number, number],
|
fromPath: [number, number],
|
||||||
toPath: [number, number]
|
toPath: [number, number]
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const section = findLovelaceContainer(
|
const section = findLovelaceContainer(config, fromPath);
|
||||||
config,
|
|
||||||
fromPath
|
|
||||||
) as LovelaceSectionRawConfig;
|
|
||||||
|
|
||||||
let newConfig = deleteSection(config, fromPath[0], fromPath[1]);
|
let newConfig = deleteSection(config, fromPath[0], fromPath[1]);
|
||||||
newConfig = insertSection(newConfig, toPath[0], toPath[1], section);
|
newConfig = insertSection(newConfig, toPath[0], toPath[1], section);
|
||||||
|
@ -46,7 +46,15 @@ export const getLovelaceContainerPath = (
|
|||||||
path: LovelaceCardPath
|
path: LovelaceCardPath
|
||||||
): LovelaceContainerPath => path.slice(0, -1) as LovelaceContainerPath;
|
): LovelaceContainerPath => path.slice(0, -1) as LovelaceContainerPath;
|
||||||
|
|
||||||
export const findLovelaceContainer = (
|
type FindLovelaceContainer = {
|
||||||
|
(config: LovelaceConfig, path: [number]): LovelaceViewRawConfig;
|
||||||
|
(config: LovelaceConfig, path: [number, number]): LovelaceSectionRawConfig;
|
||||||
|
(
|
||||||
|
config: LovelaceConfig,
|
||||||
|
path: LovelaceContainerPath
|
||||||
|
): LovelaceViewRawConfig | LovelaceSectionRawConfig;
|
||||||
|
};
|
||||||
|
export const findLovelaceContainer: FindLovelaceContainer = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
path: LovelaceContainerPath
|
path: LovelaceContainerPath
|
||||||
): LovelaceViewRawConfig | LovelaceSectionRawConfig => {
|
): LovelaceViewRawConfig | LovelaceSectionRawConfig => {
|
||||||
|
@ -0,0 +1,319 @@
|
|||||||
|
import { ActionDetail } from "@material/mwc-list";
|
||||||
|
import { mdiCheck, mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||||
|
import "@polymer/paper-tabs/paper-tab";
|
||||||
|
import "@polymer/paper-tabs/paper-tabs";
|
||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-button-menu";
|
||||||
|
import "../../../../components/ha-circular-progress";
|
||||||
|
import "../../../../components/ha-dialog";
|
||||||
|
import "../../../../components/ha-dialog-header";
|
||||||
|
import "../../../../components/ha-icon-button";
|
||||||
|
import "../../../../components/ha-list-item";
|
||||||
|
import "../../../../components/ha-yaml-editor";
|
||||||
|
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||||
|
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import {
|
||||||
|
findLovelaceContainer,
|
||||||
|
updateLovelaceContainer,
|
||||||
|
} from "../lovelace-path";
|
||||||
|
import "./hui-section-settings-editor";
|
||||||
|
import "./hui-section-visibility-editor";
|
||||||
|
import type { EditSectionDialogParams } from "./show-edit-section-dialog";
|
||||||
|
|
||||||
|
const TABS = ["tab-settings", "tab-visibility"] as const;
|
||||||
|
|
||||||
|
@customElement("hui-dialog-edit-section")
|
||||||
|
export class HuiDialogEditSection
|
||||||
|
extends LitElement
|
||||||
|
implements HassDialog<EditSectionDialogParams>
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _params?: EditSectionDialogParams;
|
||||||
|
|
||||||
|
@state() private _config?: LovelaceSectionRawConfig;
|
||||||
|
|
||||||
|
@state() private _yamlMode = false;
|
||||||
|
|
||||||
|
@state() private _curTab: (typeof TABS)[number] = TABS[0];
|
||||||
|
|
||||||
|
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues) {
|
||||||
|
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||||
|
const viewConfig = {
|
||||||
|
...this._config,
|
||||||
|
};
|
||||||
|
this._editor?.setValue(viewConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async showDialog(params: EditSectionDialogParams): Promise<void> {
|
||||||
|
this._params = params;
|
||||||
|
|
||||||
|
this._config = findLovelaceContainer(this._params.lovelaceConfig, [
|
||||||
|
this._params.viewIndex,
|
||||||
|
this._params.sectionIndex,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._params = undefined;
|
||||||
|
this._yamlMode = false;
|
||||||
|
this._config = undefined;
|
||||||
|
this._curTab = TABS[0];
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params || !this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heading = this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.edit_section.header"
|
||||||
|
);
|
||||||
|
|
||||||
|
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||||
|
|
||||||
|
if (this._yamlMode) {
|
||||||
|
content = html`
|
||||||
|
<ha-yaml-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
dialogInitialFocus
|
||||||
|
@value-changed=${this._viewYamlChanged}
|
||||||
|
></ha-yaml-editor>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
switch (this._curTab) {
|
||||||
|
case "tab-settings":
|
||||||
|
content = html`
|
||||||
|
<hui-section-settings-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.config=${this._config}
|
||||||
|
@value-changed=${this._configChanged}
|
||||||
|
>
|
||||||
|
</hui-section-settings-editor>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case "tab-visibility":
|
||||||
|
content = html`
|
||||||
|
<hui-section-visibility-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.config=${this._config}
|
||||||
|
@value-changed=${this._configChanged}
|
||||||
|
>
|
||||||
|
</hui-section-visibility-editor>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
scrimClickAction
|
||||||
|
@keydown=${this._ignoreKeydown}
|
||||||
|
@closed=${this._cancel}
|
||||||
|
.heading=${heading}
|
||||||
|
class=${classMap({
|
||||||
|
"yaml-mode": this._yamlMode,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ha-dialog-header show-border slot="heading">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
dialogAction="cancel"
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title">${heading}</span>
|
||||||
|
<ha-button-menu
|
||||||
|
slot="actionItems"
|
||||||
|
fixed
|
||||||
|
corner="BOTTOM_END"
|
||||||
|
menuCorner="END"
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
@action=${this._handleAction}
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass!.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.edit_section.edit_ui"
|
||||||
|
)}
|
||||||
|
${!this._yamlMode
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: ``}
|
||||||
|
</ha-list-item>
|
||||||
|
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.edit_section.edit_yaml"
|
||||||
|
)}
|
||||||
|
${this._yamlMode
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: ``}
|
||||||
|
</ha-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
${!this._yamlMode
|
||||||
|
? html`
|
||||||
|
<paper-tabs
|
||||||
|
scrollable
|
||||||
|
hide-scroll-buttons
|
||||||
|
.selected=${TABS.indexOf(this._curTab)}
|
||||||
|
@selected-item-changed=${this._handleTabSelected}
|
||||||
|
>
|
||||||
|
${TABS.map(
|
||||||
|
(tab, index) => html`
|
||||||
|
<paper-tab id=${tab} .dialogInitialFocus=${index === 0}>
|
||||||
|
${this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||||
|
)}
|
||||||
|
</paper-tab>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</paper-tabs>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-dialog-header>
|
||||||
|
${content}
|
||||||
|
<ha-button slot="secondaryAction">
|
||||||
|
${this.hass!.localize("ui.common.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
|
||||||
|
<ha-button slot="primaryAction" @click=${this._save}>
|
||||||
|
${this.hass!.localize("ui.common.save")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _configChanged(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._config = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleTabSelected(ev: CustomEvent): void {
|
||||||
|
if (!ev.detail.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._curTab = ev.detail.value.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._yamlMode = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._yamlMode = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _viewYamlChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (!ev.detail.isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._config = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _ignoreKeydown(ev: KeyboardEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cancel(ev?: Event) {
|
||||||
|
if (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _save(): Promise<void> {
|
||||||
|
if (!this._params || !this._config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newConfig = updateLovelaceContainer(
|
||||||
|
this._params.lovelaceConfig,
|
||||||
|
[this._params.viewIndex, this._params.sectionIndex],
|
||||||
|
this._config
|
||||||
|
);
|
||||||
|
|
||||||
|
this._params.saveConfig(newConfig);
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-dialog {
|
||||||
|
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
|
||||||
|
--vertical-align-dialog: flex-start;
|
||||||
|
--dialog-surface-margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
/* When in fullscreen dialog should be attached to top */
|
||||||
|
ha-dialog {
|
||||||
|
--dialog-surface-margin-top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ha-dialog.yaml-mode {
|
||||||
|
--dialog-content-padding: 0;
|
||||||
|
}
|
||||||
|
paper-tabs {
|
||||||
|
--paper-tabs-selection-bar-color: var(--primary-color);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
.selected_menu_item {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
@media all and (min-width: 600px) {
|
||||||
|
ha-dialog {
|
||||||
|
--mdc-dialog-min-width: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-dialog-edit-section": HuiDialogEditSection;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import {
|
||||||
|
HaFormSchema,
|
||||||
|
SchemaUnion,
|
||||||
|
} from "../../../../components/ha-form/types";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
const SCHEMA = [
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
selector: { text: {} },
|
||||||
|
},
|
||||||
|
] as const satisfies HaFormSchema[];
|
||||||
|
|
||||||
|
type SettingsData = {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hui-section-settings-editor")
|
||||||
|
export class HuiDialogEditSection extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const data: SettingsData = {
|
||||||
|
title: this.config.title || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-form
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${data}
|
||||||
|
.schema=${SCHEMA}
|
||||||
|
.computeLabel=${this._computeLabel}
|
||||||
|
.computeHelper=${this._computeHelper}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||||
|
this.hass.localize(
|
||||||
|
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
private _computeHelper = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||||
|
this.hass.localize(
|
||||||
|
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}_helper`
|
||||||
|
) || "";
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newData = ev.detail.value as SettingsData;
|
||||||
|
|
||||||
|
const newConfig: LovelaceSectionRawConfig = {
|
||||||
|
...this.config,
|
||||||
|
title: newData.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!newConfig.title) {
|
||||||
|
delete newConfig.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", { value: newConfig });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-section-settings-editor": HuiDialogEditSection;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-alert";
|
||||||
|
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { Condition } from "../../common/validate-condition";
|
||||||
|
import "../conditions/ha-card-conditions-editor";
|
||||||
|
|
||||||
|
@customElement("hui-section-visibility-editor")
|
||||||
|
export class HuiDialogEditSection extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const conditions = this.config.visibility ?? [];
|
||||||
|
return html`
|
||||||
|
<ha-alert alert-type="info">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.lovelace.editor.edit_section.visibility.explanation`
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
<ha-card-conditions-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.conditions=${conditions}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-card-conditions-editor>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const conditions = ev.detail.value as Condition[];
|
||||||
|
const newConfig: LovelaceSectionRawConfig = {
|
||||||
|
...this.config,
|
||||||
|
visibility: conditions,
|
||||||
|
};
|
||||||
|
if (newConfig.visibility?.length === 0) {
|
||||||
|
delete newConfig.visibility;
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", { value: newConfig });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-section-visibility-editor": HuiDialogEditSection;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
|
|
||||||
|
export type EditSectionDialogParams = {
|
||||||
|
lovelaceConfig: LovelaceConfig;
|
||||||
|
saveConfig: (config: LovelaceConfig) => void;
|
||||||
|
viewIndex: number;
|
||||||
|
sectionIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importEditSectionDialog = () => import("./hui-dialog-edit-section");
|
||||||
|
|
||||||
|
export const showEditSectionDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
editSectionDialogParams: EditSectionDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "hui-dialog-edit-section",
|
||||||
|
dialogImport: importEditSectionDialog,
|
||||||
|
dialogParams: editSectionDialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import { PropertyValues, ReactiveElement } from "lit";
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
@ -10,6 +11,10 @@ import {
|
|||||||
} from "../../../data/lovelace/config/section";
|
} from "../../../data/lovelace/config/section";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { HuiErrorCard } from "../cards/hui-error-card";
|
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||||
|
import {
|
||||||
|
checkConditionsMet,
|
||||||
|
attachConditionMediaQueriesListeners,
|
||||||
|
} from "../common/validate-condition";
|
||||||
import { createCardElement } from "../create-element/create-card-element";
|
import { createCardElement } from "../create-element/create-card-element";
|
||||||
import {
|
import {
|
||||||
createErrorCardConfig,
|
createErrorCardConfig,
|
||||||
@ -43,6 +48,8 @@ export class HuiSection extends ReactiveElement {
|
|||||||
|
|
||||||
private _layoutElement?: LovelaceSectionElement;
|
private _layoutElement?: LovelaceSectionElement;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
// Public to make demo happy
|
// Public to make demo happy
|
||||||
public createCardElement(cardConfig: LovelaceCardConfig) {
|
public createCardElement(cardConfig: LovelaceCardConfig) {
|
||||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||||
@ -95,6 +102,17 @@ export class HuiSection extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._listenMediaQueries();
|
||||||
|
this._updateElement();
|
||||||
|
}
|
||||||
|
|
||||||
protected update(changedProperties) {
|
protected update(changedProperties) {
|
||||||
super.update(changedProperties);
|
super.update(changedProperties);
|
||||||
|
|
||||||
@ -109,7 +127,6 @@ export class HuiSection extends ReactiveElement {
|
|||||||
this._rebuildCard(element, createErrorCardConfig(e.message, null));
|
this._rebuildCard(element, createErrorCardConfig(e.message, null));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._layoutElement.hass = this.hass;
|
this._layoutElement.hass = this.hass;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("lovelace")) {
|
if (changedProperties.has("lovelace")) {
|
||||||
@ -118,9 +135,32 @@ export class HuiSection extends ReactiveElement {
|
|||||||
if (changedProperties.has("_cards")) {
|
if (changedProperties.has("_cards")) {
|
||||||
this._layoutElement.cards = this._cards;
|
this._layoutElement.cards = this._cards;
|
||||||
}
|
}
|
||||||
|
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
|
||||||
|
this._updateElement();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearMediaQueries() {
|
||||||
|
this._listeners.forEach((unsub) => unsub());
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listenMediaQueries() {
|
||||||
|
if (!this.config.visibility) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._clearMediaQueries();
|
||||||
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
|
this.config.visibility,
|
||||||
|
this.hass,
|
||||||
|
(visibility) => {
|
||||||
|
const visible = visibility || this.lovelace!.editMode;
|
||||||
|
this._updateElement(visible);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async _initializeConfig() {
|
private async _initializeConfig() {
|
||||||
let sectionConfig = { ...this.config };
|
let sectionConfig = { ...this.config };
|
||||||
let isStrategy = false;
|
let isStrategy = false;
|
||||||
@ -161,7 +201,26 @@ export class HuiSection extends ReactiveElement {
|
|||||||
while (this.lastChild) {
|
while (this.lastChild) {
|
||||||
this.removeChild(this.lastChild);
|
this.removeChild(this.lastChild);
|
||||||
}
|
}
|
||||||
this.appendChild(this._layoutElement!);
|
this._updateElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateElement(forceVisible?: boolean) {
|
||||||
|
if (!this._layoutElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const visible =
|
||||||
|
forceVisible ??
|
||||||
|
(this.lovelace.editMode ||
|
||||||
|
!this.config.visibility ||
|
||||||
|
checkConditionsMet(this.config.visibility, this.hass));
|
||||||
|
|
||||||
|
this.style.setProperty("display", visible ? "" : "none");
|
||||||
|
this.toggleAttribute("hidden", !visible);
|
||||||
|
if (!visible && this._layoutElement.parentElement) {
|
||||||
|
this.removeChild(this._layoutElement);
|
||||||
|
} else if (visible && !this._layoutElement.parentElement) {
|
||||||
|
this.appendChild(this._layoutElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js";
|
import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js";
|
||||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
@ -7,18 +14,12 @@ import "../../../components/ha-icon-button";
|
|||||||
import "../../../components/ha-sortable";
|
import "../../../components/ha-sortable";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||||
import { LovelaceSectionConfig as LovelaceRawSectionConfig } from "../../../data/lovelace/config/section";
|
|
||||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||||
import {
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
showConfirmationDialog,
|
|
||||||
showPromptDialog,
|
|
||||||
} from "../../../dialogs/generic/show-dialog-box";
|
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { addSection, deleteSection, moveSection } from "../editor/config-util";
|
import { addSection, deleteSection, moveSection } from "../editor/config-util";
|
||||||
import {
|
import { findLovelaceContainer } from "../editor/lovelace-path";
|
||||||
findLovelaceContainer,
|
import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog";
|
||||||
updateLovelaceContainer,
|
|
||||||
} from "../editor/lovelace-path";
|
|
||||||
import { HuiSection } from "../sections/hui-section";
|
import { HuiSection } from "../sections/hui-section";
|
||||||
import type { Lovelace, LovelaceBadge } from "../types";
|
import type { Lovelace, LovelaceBadge } from "../types";
|
||||||
|
|
||||||
@ -38,27 +39,54 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@state() private _config?: LovelaceViewConfig;
|
@state() private _config?: LovelaceViewConfig;
|
||||||
|
|
||||||
|
@state() private _sectionCount = 0;
|
||||||
|
|
||||||
public setConfig(config: LovelaceViewConfig): void {
|
public setConfig(config: LovelaceViewConfig): void {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sectionConfigKeys = new WeakMap<LovelaceRawSectionConfig, string>();
|
private _sectionConfigKeys = new WeakMap<HuiSection, string>();
|
||||||
|
|
||||||
private _getKey(sectionConfig: LovelaceRawSectionConfig) {
|
private _getKey(sectionConfig: HuiSection) {
|
||||||
if (!this._sectionConfigKeys.has(sectionConfig)) {
|
if (!this._sectionConfigKeys.has(sectionConfig)) {
|
||||||
this._sectionConfigKeys.set(sectionConfig, Math.random().toString());
|
this._sectionConfigKeys.set(sectionConfig, Math.random().toString());
|
||||||
}
|
}
|
||||||
return this._sectionConfigKeys.get(sectionConfig)!;
|
return this._sectionConfigKeys.get(sectionConfig)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _sectionObserver?: MutationObserver;
|
||||||
|
|
||||||
|
private _computeSectionsCount() {
|
||||||
|
this._sectionCount = this.sections.filter(
|
||||||
|
(section) => !section.hidden
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
willUpdate(changedProperties: PropertyValues<typeof this>): void {
|
||||||
|
if (!this._sectionObserver) {
|
||||||
|
this._sectionObserver = new MutationObserver(() => {
|
||||||
|
this._computeSectionsCount();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (changedProperties.has("sections")) {
|
||||||
|
this._computeSectionsCount();
|
||||||
|
this._sectionObserver.disconnect();
|
||||||
|
this.sections.forEach((section) => {
|
||||||
|
this._sectionObserver!.observe(section, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["hidden"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.lovelace) return nothing;
|
if (!this.lovelace) return nothing;
|
||||||
|
|
||||||
const sectionsConfig = this._config?.sections ?? [];
|
const sections = this.sections;
|
||||||
|
const totalCount = this._sectionCount + (this.lovelace?.editMode ? 1 : 0);
|
||||||
const editMode = this.lovelace.editMode;
|
const editMode = this.lovelace.editMode;
|
||||||
|
|
||||||
const sectionCount = sectionsConfig.length + (editMode ? 1 : 0);
|
|
||||||
const maxColumnsCount = this._config?.max_columns;
|
const maxColumnsCount = this._config?.max_columns;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -77,14 +105,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
class="container"
|
class="container"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
"--max-columns-count": maxColumnsCount,
|
"--max-columns-count": maxColumnsCount,
|
||||||
"--total-count": sectionCount,
|
"--total-count": totalCount,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${repeat(
|
${repeat(
|
||||||
sectionsConfig,
|
sections,
|
||||||
(sectionConfig) => this._getKey(sectionConfig),
|
(section) => this._getKey(section),
|
||||||
(_sectionConfig, idx) => {
|
(section, idx) => {
|
||||||
const section = this.sections[idx];
|
|
||||||
(section as any).itemPath = [idx];
|
(section as any).itemPath = [idx];
|
||||||
return html`
|
return html`
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@ -113,7 +140,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div class="section-wrapper">${section}</div>
|
${section}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -150,39 +177,14 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
private async _editSection(ev) {
|
private async _editSection(ev) {
|
||||||
const index = ev.currentTarget.index;
|
const index = ev.currentTarget.index;
|
||||||
|
|
||||||
const path = [this.index!, index] as [number, number];
|
showEditSectionDialog(this, {
|
||||||
|
lovelaceConfig: this.lovelace!.config,
|
||||||
const section = findLovelaceContainer(
|
saveConfig: (newConfig) => {
|
||||||
this.lovelace!.config,
|
this.lovelace!.saveConfig(newConfig);
|
||||||
path
|
},
|
||||||
) as LovelaceRawSectionConfig;
|
viewIndex: this.index!,
|
||||||
|
sectionIndex: index,
|
||||||
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) {
|
private async _deleteSection(ev) {
|
||||||
@ -190,13 +192,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
const path = [this.index!, index] as [number, number];
|
const path = [this.index!, index] as [number, number];
|
||||||
|
|
||||||
const section = findLovelaceContainer(
|
const section = findLovelaceContainer(this.lovelace!.config, path);
|
||||||
this.lovelace!.config,
|
|
||||||
path
|
|
||||||
) as LovelaceRawSectionConfig;
|
|
||||||
|
|
||||||
const title = section.title?.trim();
|
const title = section.title?.trim();
|
||||||
const cardCount = section.cards?.length;
|
const cardCount = "cards" in section && section.cards?.length;
|
||||||
|
|
||||||
if (title || cardCount) {
|
if (title || cardCount) {
|
||||||
const named = title ? "named" : "unnamed";
|
const named = title ? "named" : "unnamed";
|
||||||
@ -261,6 +260,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
border-radius: var(--ha-card-border-radius, 12px);
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section:not(:has(> *:not([hidden]))) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
--max-count: min(var(--total-count), var(--max-columns-count, 4));
|
--max-count: min(var(--total-count), var(--max-columns-count, 4));
|
||||||
--max-width: min(
|
--max-width: min(
|
||||||
|
@ -148,7 +148,7 @@ export class HUIView extends ReactiveElement {
|
|||||||
this._applyTheme();
|
this._applyTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
public willUpdate(changedProperties: PropertyValues): void {
|
public willUpdate(changedProperties: PropertyValues<typeof this>): void {
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -161,7 +161,7 @@ export class HUIView extends ReactiveElement {
|
|||||||
- lovelace changes if edit mode is enabled or config has changed
|
- lovelace changes if edit mode is enabled or config has changed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const oldLovelace = changedProperties.get("lovelace") as this["lovelace"];
|
const oldLovelace = changedProperties.get("lovelace");
|
||||||
|
|
||||||
// If config has changed, create element if necessary and set all values.
|
// If config has changed, create element if necessary and set all values.
|
||||||
if (
|
if (
|
||||||
|
@ -5531,10 +5531,19 @@
|
|||||||
"text_named_section_cards": "''{name}'' section and all its cards will be deleted.",
|
"text_named_section_cards": "''{name}'' section and all its cards will be deleted.",
|
||||||
"text_unnamed_section_cards": "This section and all its cards will be deleted."
|
"text_unnamed_section_cards": "This section and all its cards will be deleted."
|
||||||
},
|
},
|
||||||
"edit_section_title": {
|
"edit_section": {
|
||||||
"title": "Edit name",
|
"header": "Edit section",
|
||||||
"title_new": "Add name",
|
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
|
||||||
"input_label": "Name"
|
"tab_settings": "[%key:ui::panel::lovelace::editor::edit_view::tab_settings%]",
|
||||||
|
"edit_ui": "[%key:ui::panel::lovelace::editor::edit_view::edit_ui%]",
|
||||||
|
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||||
|
"settings": {
|
||||||
|
"title": "Title",
|
||||||
|
"title_helper": "The title will appear at the top of section. Leave empty to hide the title."
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"suggest_card": {
|
"suggest_card": {
|
||||||
"header": "We created a suggestion for you",
|
"header": "We created a suggestion for you",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user