mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-27 16:14:03 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebc35db562 |
No files matched your search
@@ -49,6 +49,15 @@ export const normalizeViewConfig = (
|
||||
.map(normalizeSectionConfig);
|
||||
}
|
||||
|
||||
if (updatedView.sidebar?.sections) {
|
||||
updatedView.sidebar = {
|
||||
...updatedView.sidebar,
|
||||
sections: updatedView.sidebar.sections
|
||||
.filter(Boolean)
|
||||
.map(normalizeSectionConfig),
|
||||
};
|
||||
}
|
||||
|
||||
return updatedView;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { mdiDelete, mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { deleteAtPath, getAtPath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@customElement("hui-sidebar-edit-header")
|
||||
export class HuiSidebarEditHeader extends LitElement {
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="actions">
|
||||
<span class="title">
|
||||
${this._localize("ui.panel.lovelace.editor.sidebar.title")}
|
||||
</span>
|
||||
<ha-dropdown
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this._localize("ui.panel.lovelace.editor.sidebar.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this._localize("ui.common.delete")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent): void {
|
||||
const action = ev.detail?.item?.value;
|
||||
if (action === "delete") {
|
||||
this._deleteSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteSidebar() {
|
||||
const sidebar = getAtPath<LovelaceViewSidebarConfig>(
|
||||
this.lovelace.config,
|
||||
this.path
|
||||
);
|
||||
if (sidebar?.sections?.length) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this._localize("ui.panel.lovelace.editor.sidebar.delete_title"),
|
||||
text: this._localize("ui.panel.lovelace.editor.sidebar.delete_text"),
|
||||
confirmText: this._localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirm) return;
|
||||
}
|
||||
this.lovelace.saveConfig(deleteAtPath(this.lovelace.config, this.path));
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 34px;
|
||||
}
|
||||
.actions {
|
||||
position: absolute;
|
||||
height: 36px;
|
||||
bottom: -2px;
|
||||
inset-inline-end: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline-start: var(--ha-space-3);
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: var(--secondary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-sidebar-edit-header": HuiSidebarEditHeader;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||
import "../sections/hui-grid-section";
|
||||
import "../sections/hui-stack-section";
|
||||
import { createLovelaceElement } from "./create-element-base";
|
||||
|
||||
const ALWAYS_LOADED_LAYOUTS = new Set(["grid"]);
|
||||
const ALWAYS_LOADED_LAYOUTS = new Set(["grid", "stack"]);
|
||||
|
||||
const LAZY_LOAD_LAYOUTS = {};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getParentPath,
|
||||
insertAtPath,
|
||||
getPathTarget,
|
||||
isViewSectionPath,
|
||||
moveAtPath,
|
||||
pathEquals,
|
||||
setAtPath,
|
||||
@@ -102,7 +103,15 @@ export const getCardSectionConfig = (
|
||||
if (!section || isStrategySection(section)) {
|
||||
return undefined;
|
||||
}
|
||||
return section;
|
||||
if (isViewSectionPath(containerPath)) {
|
||||
return section;
|
||||
}
|
||||
// Nested sections take the width of the section placed in the view
|
||||
const viewSectionPath = containerPath.slice(0, 4);
|
||||
const viewSection = isViewSectionPath(viewSectionPath)
|
||||
? getAtPath<LovelaceSectionRawConfig>(config, viewSectionPath)
|
||||
: undefined;
|
||||
return { ...section, column_span: viewSection?.column_span };
|
||||
};
|
||||
|
||||
export const addView = (
|
||||
|
||||
@@ -40,6 +40,10 @@ export const getParentPath = (path: LovelacePath): LovelacePath =>
|
||||
export const getViewPath = (path: LovelacePath): LovelacePath =>
|
||||
path.slice(0, 2);
|
||||
|
||||
export const isViewSectionPath = (path: LovelacePath): boolean =>
|
||||
path[path.length - 2] === "sections" &&
|
||||
pathEquals(getParentPath(getParentPath(path)), getViewPath(path));
|
||||
|
||||
export const getPathTarget = (path: LovelacePath): LovelacePathTarget => {
|
||||
const last = path[path.length - 1];
|
||||
if (typeof last === "number") {
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
getAtPath,
|
||||
getViewPath,
|
||||
insertAtPath,
|
||||
isViewSectionPath,
|
||||
moveAtPath,
|
||||
setAtPath,
|
||||
} from "../lovelace-path";
|
||||
@@ -224,6 +225,7 @@ export class HuiDialogEditSection
|
||||
<hui-section-settings-editor
|
||||
.config=${this._config}
|
||||
.viewConfig=${this._viewConfig}
|
||||
.nested=${!isViewSectionPath(this._params.path)}
|
||||
@value-changed=${this._configChanged}
|
||||
>
|
||||
</hui-section-settings-editor>
|
||||
|
||||
@@ -99,6 +99,8 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
@property({ attribute: false }) public nested = false;
|
||||
|
||||
private get _maxColumns() {
|
||||
return this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS;
|
||||
}
|
||||
@@ -120,9 +122,9 @@ export class HuiDialogEditSection extends LitElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, maxColumns: number) =>
|
||||
(localize: LocalizeFunc, maxColumns: number, nested: boolean) =>
|
||||
[
|
||||
...computeColumnSpanSchemas(maxColumns, localize),
|
||||
...(nested ? [] : computeColumnSpanSchemas(maxColumns, localize)),
|
||||
{
|
||||
name: "background_enabled",
|
||||
selector: { boolean: {} },
|
||||
@@ -189,7 +191,7 @@ export class HuiDialogEditSection extends LitElement {
|
||||
theme: this.config.theme,
|
||||
};
|
||||
|
||||
const schema = this._schema(this._localize, this._maxColumns);
|
||||
const schema = this._schema(this._localize, this._maxColumns, this.nested);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
mdiFormatListBulletedTriangle,
|
||||
mdiHelpCircleOutline,
|
||||
mdiMagnify,
|
||||
mdiPageLayoutSidebarRight,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRedo,
|
||||
@@ -58,6 +59,7 @@ import type {
|
||||
} from "../../data/lovelace/config/types";
|
||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||
import { isStrategyView } from "../../data/lovelace/config/view";
|
||||
import {
|
||||
deleteDashboard,
|
||||
fetchDashboards,
|
||||
@@ -87,6 +89,7 @@ import { showAddIntegrationDialog } from "../config/integrations/show-add-integr
|
||||
import { showDashboardDetailDialog } from "../config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail";
|
||||
import { showPersonDetailDialog } from "../config/person/show-dialog-person-detail";
|
||||
import { swapView } from "./editor/config-util";
|
||||
import { setAtPath } from "./editor/lovelace-path";
|
||||
import { showDashboardStrategyEditorDialog } from "./editor/dashboard-strategy-editor/dialogs/show-dialog-dashboard-strategy-editor";
|
||||
import { showSaveDialog } from "./editor/show-save-config-dialog";
|
||||
import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
|
||||
@@ -97,6 +100,8 @@ import "./views/hui-view";
|
||||
import type { HUIView } from "./views/hui-view";
|
||||
import "./views/hui-view-background";
|
||||
import "./views/hui-view-container";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "./views/const";
|
||||
import { getViewType } from "./views/get-view-type";
|
||||
import { LOVELACE_EDIT_TOAST_ID } from "./types";
|
||||
|
||||
interface ActionItem {
|
||||
@@ -219,7 +224,24 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
const isLovelaceDashboard = this.panel?.component_name === "lovelace";
|
||||
|
||||
const curViewConfig =
|
||||
typeof this._curView === "number"
|
||||
? this.lovelace?.config.views[this._curView]
|
||||
: undefined;
|
||||
const canAddSidebar =
|
||||
curViewConfig !== undefined &&
|
||||
!isStrategyView(curViewConfig) &&
|
||||
getViewType(curViewConfig) === SECTIONS_VIEW_LAYOUT &&
|
||||
!curViewConfig.sidebar;
|
||||
|
||||
const items: ActionItem[] = [
|
||||
{
|
||||
icon: mdiPageLayoutSidebarRight,
|
||||
key: "ui.panel.lovelace.editor.menu.add_sidebar",
|
||||
overflowAction: this._handleAddSidebar,
|
||||
visible: this._editMode && canAddSidebar,
|
||||
overflow: true,
|
||||
},
|
||||
{
|
||||
icon: mdiFormatListBulletedTriangle,
|
||||
key: "ui.panel.lovelace.unused_entities.title",
|
||||
@@ -972,6 +994,13 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAddSidebar = () => {
|
||||
const sidebarPath = ["views", this._curView as number, "sidebar"];
|
||||
this.lovelace!.saveConfig(
|
||||
setAtPath(this.lovelace!.config, sidebarPath, { sections: [] })
|
||||
);
|
||||
};
|
||||
|
||||
private _handleRawEditor = () => {
|
||||
this.lovelace!.enableFullEditMode();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { mdiViewGridPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/hui-section-edit-mode";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { moveAtPath } from "../editor/lovelace-path";
|
||||
import { showCreateSectionDialog } from "../editor/section-editor/show-create-section-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
import type { HuiSection } from "./hui-section";
|
||||
import {
|
||||
renderSectionContainer,
|
||||
sectionContainerStyles,
|
||||
} from "./section-container";
|
||||
|
||||
@customElement("hui-stack-section")
|
||||
export class HuiStackSection
|
||||
extends LitElement
|
||||
implements LovelaceSectionElement
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@property({ attribute: false }) public isStrategy = false;
|
||||
|
||||
@property({ attribute: false }) public sections: HuiSection[] = [];
|
||||
|
||||
public setConfig(_config: LovelaceSectionConfig): void {
|
||||
// The stack has no settings of its own
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { hass, lovelace, path } = this;
|
||||
if (!hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const editMode = Boolean(lovelace?.editMode && !this.isStrategy);
|
||||
if (!editMode || !lovelace || !path) {
|
||||
return html`
|
||||
<div class="sections">
|
||||
${repeat(
|
||||
this.sections,
|
||||
(section) => section,
|
||||
(section) =>
|
||||
html`<div class="section">
|
||||
${renderSectionContainer(hass, section.config, section)}
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const sectionsPath = [...path, "sections"];
|
||||
|
||||
return html`
|
||||
<ha-sortable
|
||||
@item-moved=${this._sectionMoved}
|
||||
@item-added=${this._sectionAdded}
|
||||
@item-removed=${this._sectionRemoved}
|
||||
group="section"
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".section"
|
||||
>
|
||||
<div class="sections">
|
||||
${repeat(
|
||||
this.sections,
|
||||
(section) => section,
|
||||
(section, idx) => html`
|
||||
<div class="section" .sortableData=${[...sectionsPath, idx]}>
|
||||
<hui-section-edit-mode
|
||||
.hass=${hass}
|
||||
.lovelace=${lovelace}
|
||||
.path=${[...sectionsPath, idx]}
|
||||
.isStrategy=${isStrategySection(section.config)}
|
||||
>
|
||||
${renderSectionContainer(hass, section.config, section)}
|
||||
</hui-section-edit-mode>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<button
|
||||
class="create-section"
|
||||
@click=${this._createSection}
|
||||
aria-label=${hass.localize(
|
||||
"ui.panel.lovelace.editor.section.create_section"
|
||||
)}
|
||||
.title=${hass.localize(
|
||||
"ui.panel.lovelace.editor.section.create_section"
|
||||
)}
|
||||
>
|
||||
<ha-ripple></ha-ripple>
|
||||
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ha-sortable>
|
||||
`;
|
||||
}
|
||||
|
||||
private _sectionMoved(ev: HASSDomEvent<HASSDomEvents["item-moved"]>) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const sectionsPath = [...this.path!, "sections"];
|
||||
this.lovelace!.saveConfig(
|
||||
moveAtPath(
|
||||
this.lovelace!.config,
|
||||
[...sectionsPath, oldIndex],
|
||||
[...sectionsPath, newIndex]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _sectionAdded(ev: HASSDomEvent<HASSDomEvents["item-added"]>) {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
this.lovelace!.saveConfig(
|
||||
moveAtPath(this.lovelace!.config, data as LovelacePath, [
|
||||
...this.path!,
|
||||
"sections",
|
||||
index,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
private _sectionRemoved(ev: HASSDomEvent<HASSDomEvents["item-removed"]>) {
|
||||
// The list the section is dropped in moves it
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _createSection() {
|
||||
showCreateSectionDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
containerPath: this.path!,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
sectionContainerStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--row-gap, 8px);
|
||||
}
|
||||
.section:has(hui-section[hidden]) {
|
||||
display: none;
|
||||
}
|
||||
.create-section {
|
||||
display: block;
|
||||
position: relative;
|
||||
outline: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
border: 2px dashed var(--primary-color);
|
||||
height: calc(var(--row-height, 56px) + 2 * (var(--row-gap, 8px) + 2px));
|
||||
padding: var(--ha-space-2);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
--ha-ripple-color: var(--primary-color);
|
||||
--ha-ripple-hover-opacity: 0.04;
|
||||
--ha-ripple-pressed-opacity: 0.12;
|
||||
}
|
||||
.create-section:focus {
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
.sortable-ghost {
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-stack-section": HuiStackSection;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { clamp } from "../../../common/number/clamp";
|
||||
import { getHistoryState, updateHistoryState } from "../../../common/navigate";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -17,19 +18,25 @@ import "../../../components/ha-svg-icon";
|
||||
import { maxColumnsContext } from "../common/context";
|
||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import {
|
||||
DEFAULT_SECTION_COLUMN_SPAN,
|
||||
isStrategySection,
|
||||
} from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewSidebarConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import { DEFAULT_MAX_COLUMNS } from "./const";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiBadge } from "../badges/hui-badge";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import "../components/hui-section-edit-mode";
|
||||
import "../components/hui-sidebar-edit-header";
|
||||
import { addSection } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { getAtPath, moveAtPath } from "../editor/lovelace-path";
|
||||
import "../sections/hui-section";
|
||||
import { showCreateSectionDialog } from "../editor/section-editor/show-create-section-dialog";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import {
|
||||
@@ -40,7 +47,6 @@ import type { Lovelace } from "../types";
|
||||
import { generateDefaultSection } from "./default-section";
|
||||
import "./hui-view-footer";
|
||||
import "./hui-view-header";
|
||||
import "./hui-view-sidebar";
|
||||
import { computeSectionsBackgroundAlignment } from "./sections-background-alignment";
|
||||
|
||||
const parsePx = (value: string) => parseInt(value.replace("px", ""));
|
||||
@@ -181,8 +187,11 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
const sections = this.sections;
|
||||
const editMode = this.lovelace.editMode;
|
||||
const sidebar = this._config?.sidebar;
|
||||
const sidebarEditMode = editMode && !this.isStrategy;
|
||||
const hasSidebar =
|
||||
this._config?.sidebar && (this._sidebarVisible || editMode);
|
||||
sidebar &&
|
||||
(editMode || (this._sidebarVisible && Boolean(sidebar.sections?.length)));
|
||||
const singleColumn = this._maxColumns <= 1;
|
||||
|
||||
// The tab switcher is opt-in: a sidebar enables it by labelling both tabs,
|
||||
@@ -258,6 +267,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<ha-sortable
|
||||
.disabled=${!editMode}
|
||||
@item-moved=${this._sectionMoved}
|
||||
@item-added=${this._sectionAdded}
|
||||
@item-removed=${this._sectionRemoved}
|
||||
group="section"
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".section"
|
||||
@@ -281,6 +292,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
return html`
|
||||
<div
|
||||
class="section"
|
||||
.sortableData=${[...this.path!, "sections", idx]}
|
||||
style=${styleMap({
|
||||
"--column-span": columnSpan,
|
||||
"--row-span": rowSpan,
|
||||
@@ -348,23 +360,41 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
</div>
|
||||
</ha-sortable>
|
||||
${
|
||||
this._config?.sidebar
|
||||
sidebar
|
||||
? html`
|
||||
<hui-view-sidebar
|
||||
<div
|
||||
class=${classMap({
|
||||
sidebar: true,
|
||||
"edit-mode": sidebarEditMode,
|
||||
hidden:
|
||||
!hasSidebar ||
|
||||
(useSidebarTabs && !this._sidebarTabActive),
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${this.path!}
|
||||
.config=${this._config.sidebar}
|
||||
@sidebar-visibility-changed=${
|
||||
this._handleSidebarVisibilityChanged
|
||||
>
|
||||
${
|
||||
sidebarEditMode
|
||||
? html`
|
||||
<hui-sidebar-edit-header
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${this._sidebarPath(this.path!)}
|
||||
></hui-sidebar-edit-header>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
></hui-view-sidebar>
|
||||
<div class="sidebar-content">
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.config=${this._sidebarSectionConfig(sidebar)}
|
||||
.path=${this._sidebarPath(this.path!)}
|
||||
.preview=${editMode}
|
||||
.insideStrategy=${this.isStrategy}
|
||||
@section-visibility-changed=${
|
||||
this._handleSidebarVisibilityChanged
|
||||
}
|
||||
></hui-section>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -481,6 +511,22 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _sectionAdded(ev: HASSDomEvent<HASSDomEvents["item-added"]>) {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const newConfig = moveAtPath(this.lovelace!.config, data as LovelacePath, [
|
||||
...this.path!,
|
||||
"sections",
|
||||
index,
|
||||
]);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _sectionRemoved(ev: HASSDomEvent<HASSDomEvents["item-removed"]>) {
|
||||
// The list the section is dropped in moves it
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _viewChanged(ev: CustomEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
const shouldShowSidebar = newValue === "sidebar";
|
||||
@@ -512,12 +558,30 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
});
|
||||
}
|
||||
|
||||
// A stack section also hides when all its sections are hidden
|
||||
private _sidebarSectionConfig = memoizeOne(
|
||||
(sidebar: LovelaceViewSidebarConfig): LovelaceSectionConfig => ({
|
||||
type: "stack",
|
||||
sections: sidebar.sections,
|
||||
visibility: sidebar.visibility,
|
||||
})
|
||||
);
|
||||
|
||||
private _sidebarPath = memoizeOne((path: LovelacePath): LovelacePath => [
|
||||
...path,
|
||||
"sidebar",
|
||||
]);
|
||||
|
||||
private _handleSidebarVisibilityChanged = (
|
||||
e: CustomEvent<{ visible: boolean }>
|
||||
ev: CustomEvent<{ value: boolean }>
|
||||
) => {
|
||||
this._sidebarVisible = e.detail.visible;
|
||||
// Sections inside the sidebar fire the same event
|
||||
if (ev.target !== ev.currentTarget) {
|
||||
return;
|
||||
}
|
||||
this._sidebarVisible = ev.detail.value;
|
||||
// Reset sidebar tab when sidebar becomes hidden
|
||||
if (!e.detail.visible) {
|
||||
if (!ev.detail.value) {
|
||||
this._sidebarTabActive = false;
|
||||
}
|
||||
};
|
||||
@@ -575,10 +639,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: [content-start] repeat(
|
||||
var(--content-column-count),
|
||||
1fr
|
||||
);
|
||||
grid-template-columns:
|
||||
[content-start] repeat(var(--content-column-count), 1fr)
|
||||
[content-end];
|
||||
gap: var(--row-gap) var(--column-gap);
|
||||
padding: var(--row-gap) 0;
|
||||
align-items: flex-start;
|
||||
@@ -588,7 +651,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.wrapper.has-sidebar .container {
|
||||
grid-template-columns:
|
||||
[content-start] repeat(var(--content-column-count), 1fr)
|
||||
[sidebar-start] 1fr;
|
||||
[content-end sidebar-start] 1fr [sidebar-end];
|
||||
}
|
||||
|
||||
/* With a single column, content and sidebar stack at full width */
|
||||
@@ -596,15 +659,29 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
hui-view-sidebar {
|
||||
grid-column: sidebar-start / -1;
|
||||
.sidebar {
|
||||
grid-column: sidebar-start / sidebar-end;
|
||||
}
|
||||
|
||||
.wrapper.single-column hui-view-sidebar {
|
||||
.sidebar hui-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar.edit-mode .sidebar-content {
|
||||
padding: var(--ha-space-2);
|
||||
border: 2px dashed var(--divider-color);
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
.wrapper.single-column .sidebar {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.wrapper.has-sidebar-tabs hui-view-sidebar {
|
||||
.wrapper.has-sidebar-tabs .sidebar {
|
||||
padding-bottom: calc(
|
||||
var(--ha-space-14) + var(--ha-space-3) + var(--safe-area-inset-bottom)
|
||||
);
|
||||
@@ -641,7 +718,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-column: content-start / sidebar-start;
|
||||
grid-column: content-start / content-end;
|
||||
grid-row: 1 / -1;
|
||||
display: grid;
|
||||
align-items: start;
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../sections/hui-section";
|
||||
import {
|
||||
renderSectionContainer,
|
||||
sectionContainerStyles,
|
||||
} from "../sections/section-container";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
|
||||
export const DEFAULT_VIEW_SIDEBAR_LAYOUT = "start";
|
||||
|
||||
@customElement("hui-view-sidebar")
|
||||
export class HuiViewSidebar extends ConditionalListenerMixin<LovelaceViewSidebarConfig>(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewSidebarConfig;
|
||||
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
private _visible = true;
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("hass") || changedProperties.has("config")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
if (!this.hass || !this.config) return;
|
||||
|
||||
const visible = conditionsMet ?? this._conditionsVisible();
|
||||
|
||||
if (visible !== this._visible) {
|
||||
this._visible = visible;
|
||||
fireEvent(this, "sidebar-visibility-changed", { visible });
|
||||
}
|
||||
}
|
||||
|
||||
private _sectionConfigKeys = new WeakMap<LovelaceSectionConfig, string>();
|
||||
|
||||
private _getSectionKey(section: LovelaceSectionConfig) {
|
||||
if (!this._sectionConfigKeys.has(section)) {
|
||||
this._sectionConfigKeys.set(section, Math.random().toString());
|
||||
}
|
||||
return this._sectionConfigKeys.get(section)!;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.lovelace) return nothing;
|
||||
|
||||
// Use preview mode instead of setting lovelace to avoid the sections to be
|
||||
// editable as it is not yet supported
|
||||
return html`
|
||||
<div class="container">
|
||||
${repeat(
|
||||
this.config?.sections ?? [],
|
||||
(section) => this._getSectionKey(section),
|
||||
(section, idx) => {
|
||||
const sectionPath = [...this.path, "sidebar", "sections", idx];
|
||||
const sectionElement = html`
|
||||
<hui-section
|
||||
.config=${section}
|
||||
.hass=${this.hass}
|
||||
.preview=${this.lovelace.editMode}
|
||||
.path=${sectionPath}
|
||||
></hui-section>
|
||||
`;
|
||||
return renderSectionContainer(this.hass, section, sectionElement);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
sectionContainerStyles,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--row-gap, 8px);
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-view-sidebar": HuiViewSidebar;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"sidebar-visibility-changed": { visible: boolean };
|
||||
}
|
||||
}
|
||||
@@ -10199,6 +10199,7 @@
|
||||
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
|
||||
"menu": {
|
||||
"open": "Open dashboard menu",
|
||||
"add_sidebar": "Add sidebar",
|
||||
"raw_editor": "Raw configuration editor",
|
||||
"manage_dashboards": "Manage dashboards",
|
||||
"manage_resources": "Manage resources"
|
||||
@@ -10532,6 +10533,12 @@
|
||||
"built_in": "Built-in sections",
|
||||
"custom": "Community sections"
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "Sidebar",
|
||||
"menu": "Sidebar menu",
|
||||
"delete_title": "Delete sidebar",
|
||||
"delete_text": "The sidebar and all its sections will be deleted."
|
||||
},
|
||||
"edit_section": {
|
||||
"header": "Section configuration",
|
||||
"typed_header": "{type} section configuration",
|
||||
|
||||
Reference in new issue
Block a user