mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-09 01:17:48 +00:00
Pull divider and panel list out into their own components
This commit is contained in:
parent
24f334bfe2
commit
6ec35380ac
25
src/components/ha-divider-list-item.ts
Normal file
25
src/components/ha-divider-list-item.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ListItem } from "@material/mwc-list/mwc-list-item";
|
||||||
|
import { css, CSSResult, customElement } from "lit-element";
|
||||||
|
|
||||||
|
@customElement("ha-divider-list-item")
|
||||||
|
export class HaDividerListItem extends ListItem {
|
||||||
|
noninteractive = true;
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
height: var(--ha-divider-height, 48px);
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-divider-list-item": HaDividerListItem;
|
||||||
|
}
|
||||||
|
}
|
76
src/components/ha-sidebar-header.ts
Normal file
76
src/components/ha-sidebar-header.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import "./ha-sidebar-panel-list";
|
||||||
|
import "./ha-clickable-list-item";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import "@material/mwc-list/mwc-list";
|
||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import "@material/mwc-icon-button";
|
||||||
|
import { mdiMenu, mdiMenuOpen } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-menu-button";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import "./user/ha-user-badge";
|
||||||
|
|
||||||
|
@customElement("ha-sidebar-header")
|
||||||
|
class HaSidebarHeader extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@property() public text: TemplateResult | string = "";
|
||||||
|
|
||||||
|
@property() public toggleButtonCallback?: (ev: CustomEvent) => void;
|
||||||
|
|
||||||
|
// property used only in css
|
||||||
|
// @ts-ignore
|
||||||
|
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${!this.narrow
|
||||||
|
? html`
|
||||||
|
<mwc-icon-button
|
||||||
|
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||||
|
@action=${this.toggleButtonCallback}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${this.hass.dockedSidebar === "docked"
|
||||||
|
? mdiMenuOpen
|
||||||
|
: mdiMenu}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="title">
|
||||||
|
${this.text}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [haStyleScrollbar, css``];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-sidebar-header": HaSidebarHeader;
|
||||||
|
}
|
||||||
|
}
|
331
src/components/ha-sidebar-overhaul.ts
Normal file
331
src/components/ha-sidebar-overhaul.ts
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import "./ha-sidebar-header";
|
||||||
|
import "./ha-sidebar-panel-list";
|
||||||
|
import "./ha-clickable-list-item";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import "@material/mwc-list/mwc-list";
|
||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import "@material/mwc-icon-button";
|
||||||
|
import { mdiBell, mdiMenu, mdiMenuOpen } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
eventOptions,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit-element";
|
||||||
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
import { ActionHandlerDetail } from "../data/lovelace";
|
||||||
|
import {
|
||||||
|
PersistentNotification,
|
||||||
|
subscribeNotifications,
|
||||||
|
} from "../data/persistent_notification";
|
||||||
|
import { getExternalConfig } from "../external_app/external_config";
|
||||||
|
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-menu-button";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import "./user/ha-user-badge";
|
||||||
|
import { sidebarStyles } from "./ha-sidebar";
|
||||||
|
|
||||||
|
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||||
|
|
||||||
|
let Sortable;
|
||||||
|
|
||||||
|
@customElement("ha-sidebar-overhaul")
|
||||||
|
class HaSidebarOverhaul extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public alwaysExpand = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public editMode = false;
|
||||||
|
|
||||||
|
@internalProperty() private _notifications?: PersistentNotification[];
|
||||||
|
|
||||||
|
// property used only in css
|
||||||
|
// @ts-ignore
|
||||||
|
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||||
|
|
||||||
|
private _sortable?;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const hass = this.hass;
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
return html` ${this._renderHeader()}
|
||||||
|
<div class="all-panels">
|
||||||
|
<div class="panels">
|
||||||
|
<mwc-list>
|
||||||
|
<mwc-list-item>Panel 1</mwc-list-item>
|
||||||
|
<mwc-list-item>Panel 2</mwc-list-item>
|
||||||
|
<mwc-list-item>Panel 3</mwc-list-item>
|
||||||
|
<mwc-list-item noninteractive class="divider">Divider</mwc-list-item>
|
||||||
|
<mwc-list-item >Utility</mwc-list-item>
|
||||||
|
</mwc-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-stuff">User stuff</div>
|
||||||
|
<div id="sortable"><span data-panel="1"></span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderHeader() {
|
||||||
|
return html`<div
|
||||||
|
class="menu"
|
||||||
|
@action=${this._handleAction}
|
||||||
|
.actionHandler=${actionHandler({
|
||||||
|
hasHold: !this.editMode,
|
||||||
|
disabled: this.editMode,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${!this.narrow
|
||||||
|
? html`
|
||||||
|
<mwc-icon-button
|
||||||
|
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||||
|
@action=${this._toggleSidebar}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${this.hass.dockedSidebar === "docked"
|
||||||
|
? mdiMenuOpen
|
||||||
|
: mdiMenu}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="title">
|
||||||
|
${this.editMode
|
||||||
|
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||||
|
${this.hass.localize("ui.sidebar.done")}
|
||||||
|
</mwc-button>`
|
||||||
|
: "Home Assistant"}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _editDoneButton() {
|
||||||
|
return html`<mwc-button outlined @click=${() => this._closeEditMode()}>
|
||||||
|
${this.hass.localize("ui.sidebar.done")}
|
||||||
|
</mwc-button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("alwaysExpand")) {
|
||||||
|
this.expanded = this.alwaysExpand;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("editMode")) {
|
||||||
|
if (this.editMode) {
|
||||||
|
this._activateEditMode();
|
||||||
|
} else {
|
||||||
|
this._deactivateEditMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changedProps.has("hass")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||||
|
this.rtl = computeRTL(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
|
||||||
|
// const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
|
||||||
|
// if (selectedEl) {
|
||||||
|
// // @ts-ignore
|
||||||
|
// selectedEl.scrollIntoViewIfNeeded();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _activateEditMode() {
|
||||||
|
if (!Sortable) {
|
||||||
|
const [sortableImport, sortStylesImport] = await Promise.all([
|
||||||
|
import("sortablejs/modular/sortable.core.esm"),
|
||||||
|
import("../resources/ha-sortable-style-ha-clickable"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.innerHTML = sortStylesImport.sortableStyles.cssText;
|
||||||
|
this.shadowRoot!.appendChild(style);
|
||||||
|
|
||||||
|
Sortable = sortableImport.Sortable;
|
||||||
|
Sortable.mount(sortableImport.OnSpill);
|
||||||
|
Sortable.mount(sortableImport.AutoScroll());
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateComplete;
|
||||||
|
|
||||||
|
this._createSortable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createSortable() {
|
||||||
|
this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), {
|
||||||
|
animation: 150,
|
||||||
|
fallbackClass: "sortable-fallback",
|
||||||
|
// fallbackTolerance: 15,
|
||||||
|
dataIdAttr: "data-panel",
|
||||||
|
handle: "span",
|
||||||
|
onSort: async () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deactivateEditMode() {
|
||||||
|
this._sortable?.destroy();
|
||||||
|
this._sortable = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
|
||||||
|
if (ev.detail.action !== "hold") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "hass-edit-sidebar", { editMode: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closeEditMode() {
|
||||||
|
fireEvent(this, "hass-edit-sidebar", { editMode: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleSidebar(ev: CustomEvent) {
|
||||||
|
if (ev.detail.action !== "tap") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fireEvent(this, "hass-toggle-menu");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
/* height: calc(100% - var(--header-height)); */
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
border-right: 1px solid var(--divider-color);
|
||||||
|
background-color: var(--sidebar-background-color);
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
width: 256px;
|
||||||
|
width: calc(256px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
|
:host([rtl]) {
|
||||||
|
border-right: 0;
|
||||||
|
border-left: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
.menu {
|
||||||
|
height: var(--header-height);
|
||||||
|
display: flex;
|
||||||
|
padding: 0 8.5px;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
font-size: 20px;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: calc(8.5px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([rtl]) .menu {
|
||||||
|
padding-left: 8.5px;
|
||||||
|
padding-right: calc(8.5px + env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
:host([expanded]) .menu {
|
||||||
|
width: calc(256px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
|
:host([rtl][expanded]) .menu {
|
||||||
|
width: calc(256px + env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
.menu mwc-icon-button {
|
||||||
|
color: var(--sidebar-icon-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) .menu mwc-icon-button {
|
||||||
|
margin-right: 23px;
|
||||||
|
}
|
||||||
|
:host([expanded][rtl]) .menu mwc-icon-button {
|
||||||
|
margin-right: 0px;
|
||||||
|
margin-left: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:host([narrow]) .title {
|
||||||
|
padding: 0 0px;
|
||||||
|
}
|
||||||
|
:host([expanded]) .title {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
.title mwc-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sortable,
|
||||||
|
.hidden-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-panels {
|
||||||
|
border: 2px solid black;
|
||||||
|
height: calc(100% - var(--header-height) - 132px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.panels {
|
||||||
|
border: 1px solid blue;
|
||||||
|
/* height: calc(100% - var(--header-height)); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border: 1px solid pink;
|
||||||
|
/* height: calc(100% - var(--header-height)); */
|
||||||
|
}
|
||||||
|
.utilities {
|
||||||
|
border: 1px solid red;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.user-stuff {
|
||||||
|
border: 1px solid green;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// static get styles(): CSSResult[] {
|
||||||
|
// return [haStyleScrollbar, sidebarStyles];
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-sidebar-overhaul": HaSidebarOverhaul;
|
||||||
|
}
|
||||||
|
}
|
888
src/components/ha-sidebar-panel-list.ts
Normal file
888
src/components/ha-sidebar-panel-list.ts
Normal file
@ -0,0 +1,888 @@
|
|||||||
|
import "./ha-divider-list-item";
|
||||||
|
import "./ha-clickable-list-item";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import "@material/mwc-list/mwc-list";
|
||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import "@material/mwc-icon-button";
|
||||||
|
import { mdiCellphoneCog, mdiClose, mdiPlus, mdiViewDashboard } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
eventOptions,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit-element";
|
||||||
|
import { guard } from "lit-html/directives/guard";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { LocalStorage } from "../common/decorators/local-storage";
|
||||||
|
import { compare } from "../common/string/compare";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
import {
|
||||||
|
ExternalConfig,
|
||||||
|
getExternalConfig,
|
||||||
|
} from "../external_app/external_config";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant, PanelInfo } from "../types";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-menu-button";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import "./user/ha-user-badge";
|
||||||
|
|
||||||
|
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||||
|
|
||||||
|
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||||
|
|
||||||
|
const SORT_VALUE_URL_PATHS = {
|
||||||
|
map: 1,
|
||||||
|
logbook: 2,
|
||||||
|
history: 3,
|
||||||
|
"developer-tools": 9,
|
||||||
|
hassio: 10,
|
||||||
|
config: 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
const panelSorter = (
|
||||||
|
reverseSort: string[],
|
||||||
|
defaultPanel: string,
|
||||||
|
a: PanelInfo,
|
||||||
|
b: PanelInfo
|
||||||
|
) => {
|
||||||
|
const indexA = reverseSort.indexOf(a.url_path);
|
||||||
|
const indexB = reverseSort.indexOf(b.url_path);
|
||||||
|
if (indexA !== indexB) {
|
||||||
|
if (indexA < indexB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return defaultPanelSorter(defaultPanel, a, b);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultPanelSorter = (
|
||||||
|
defaultPanel: string,
|
||||||
|
a: PanelInfo,
|
||||||
|
b: PanelInfo
|
||||||
|
) => {
|
||||||
|
// Put all the Lovelace at the top.
|
||||||
|
const aLovelace = a.component_name === "lovelace";
|
||||||
|
const bLovelace = b.component_name === "lovelace";
|
||||||
|
|
||||||
|
if (a.url_path === defaultPanel) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.url_path === defaultPanel) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aLovelace && bLovelace) {
|
||||||
|
return compare(a.title!, b.title!);
|
||||||
|
}
|
||||||
|
if (aLovelace && !bLovelace) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (bLovelace) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aBuiltIn = a.url_path in SORT_VALUE_URL_PATHS;
|
||||||
|
const bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS;
|
||||||
|
|
||||||
|
if (aBuiltIn && bBuiltIn) {
|
||||||
|
return SORT_VALUE_URL_PATHS[a.url_path] - SORT_VALUE_URL_PATHS[b.url_path];
|
||||||
|
}
|
||||||
|
if (aBuiltIn) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (bBuiltIn) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// both not built in, sort by title
|
||||||
|
return compare(a.title!, b.title!);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computePanels = memoizeOne(
|
||||||
|
(
|
||||||
|
panels: HomeAssistant["panels"],
|
||||||
|
defaultPanel: HomeAssistant["defaultPanel"],
|
||||||
|
panelsOrder: string[],
|
||||||
|
hiddenPanels: string[]
|
||||||
|
): [PanelInfo[], PanelInfo[]] => {
|
||||||
|
if (!panels) {
|
||||||
|
return [[], []];
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeSpacer: PanelInfo[] = [];
|
||||||
|
const afterSpacer: PanelInfo[] = [];
|
||||||
|
|
||||||
|
Object.values(panels).forEach((panel) => {
|
||||||
|
if (
|
||||||
|
hiddenPanels.includes(panel.url_path) ||
|
||||||
|
(!panel.title && panel.url_path !== defaultPanel)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||||
|
? afterSpacer
|
||||||
|
: beforeSpacer
|
||||||
|
).push(panel);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reverseSort = [...panelsOrder].reverse();
|
||||||
|
|
||||||
|
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
||||||
|
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
||||||
|
|
||||||
|
return [beforeSpacer, afterSpacer];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let Sortable;
|
||||||
|
|
||||||
|
@customElement("ha-sidebar-panel-list")
|
||||||
|
class HaSidebarPanelList extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public alwaysExpand = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public editMode = false;
|
||||||
|
|
||||||
|
// property used only in css
|
||||||
|
// @ts-ignore
|
||||||
|
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||||
|
|
||||||
|
@internalProperty() private _renderEmptySortable = false;
|
||||||
|
|
||||||
|
@internalProperty() private _externalConfig?: ExternalConfig;
|
||||||
|
|
||||||
|
private _mouseLeaveTimeout?: number;
|
||||||
|
|
||||||
|
private _tooltipHideTimeout?: number;
|
||||||
|
|
||||||
|
private _recentKeydownActiveUntil = 0;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
@LocalStorage("sidebarPanelOrder", true, {
|
||||||
|
attribute: false,
|
||||||
|
})
|
||||||
|
private _panelOrder: string[] = [];
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
@LocalStorage("sidebarHiddenPanels", true, {
|
||||||
|
attribute: false,
|
||||||
|
})
|
||||||
|
private _hiddenPanels: string[] = [];
|
||||||
|
|
||||||
|
private _sortable?;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
return html`
|
||||||
|
${this._renderNormalPanels()}
|
||||||
|
${this._renderUtilityPanels()}
|
||||||
|
<div class="tooltip"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
if (
|
||||||
|
changedProps.has("expanded") ||
|
||||||
|
changedProps.has("narrow") ||
|
||||||
|
changedProps.has("alwaysExpand") ||
|
||||||
|
changedProps.has("_externalConfig") ||
|
||||||
|
changedProps.has("_notifications") ||
|
||||||
|
changedProps.has("editMode") ||
|
||||||
|
changedProps.has("_renderEmptySortable") ||
|
||||||
|
changedProps.has("_hiddenPanels") ||
|
||||||
|
(changedProps.has("_panelOrder") && !this.editMode)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!this.hass || !changedProps.has("hass")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||||
|
if (!oldHass) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hass = this.hass;
|
||||||
|
return (
|
||||||
|
hass.panels !== oldHass.panels ||
|
||||||
|
hass.panelUrl !== oldHass.panelUrl ||
|
||||||
|
hass.user !== oldHass.user ||
|
||||||
|
hass.localize !== oldHass.localize ||
|
||||||
|
hass.language !== oldHass.language ||
|
||||||
|
hass.states !== oldHass.states ||
|
||||||
|
hass.defaultPanel !== oldHass.defaultPanel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
|
if (this.hass && this.hass.auth.external) {
|
||||||
|
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||||
|
this._externalConfig = conf;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("alwaysExpand")) {
|
||||||
|
this.expanded = this.alwaysExpand;
|
||||||
|
}
|
||||||
|
if (changedProps.has("editMode")) {
|
||||||
|
if (this.editMode) {
|
||||||
|
this._activateEditMode();
|
||||||
|
} else {
|
||||||
|
this._deactivateEditMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changedProps.has("hass")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
if (!oldHass || oldHass.language !== this.hass.language) {
|
||||||
|
this.rtl = computeRTL(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
|
||||||
|
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
|
||||||
|
if (selectedEl) {
|
||||||
|
// @ts-ignore
|
||||||
|
selectedEl.scrollIntoViewIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderNormalPanels() {
|
||||||
|
const [beforeSpacer] = computePanels(
|
||||||
|
this.hass.panels,
|
||||||
|
this.hass.defaultPanel,
|
||||||
|
this._panelOrder,
|
||||||
|
this._hiddenPanels
|
||||||
|
);
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
return html`
|
||||||
|
<mwc-list
|
||||||
|
attr-for-selected="data-panel"
|
||||||
|
class="ha-scrollbar"
|
||||||
|
@focusin=${this._listboxFocusIn}
|
||||||
|
@focusout=${this._listboxFocusOut}
|
||||||
|
@scroll=${this._listboxScroll}
|
||||||
|
@keydown=${this._listboxKeydown}
|
||||||
|
>
|
||||||
|
${this.editMode
|
||||||
|
? this._renderPanelsEdit(beforeSpacer)
|
||||||
|
: this._renderPanels(beforeSpacer)}
|
||||||
|
|
||||||
|
<ha-divider-list-item></ha-divider-list-item>
|
||||||
|
|
||||||
|
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||||
|
// prettier-ignore
|
||||||
|
return html`<div id="sortable">
|
||||||
|
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||||
|
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${this._renderSpacer()}
|
||||||
|
${this._renderHiddenPanels()} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderHiddenPanels() {
|
||||||
|
return html` ${this._hiddenPanels.length
|
||||||
|
? html`${this._hiddenPanels.map((url) => {
|
||||||
|
const panel = this.hass.panels[url];
|
||||||
|
if (!panel) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return html`<ha-clickable-list-item
|
||||||
|
@click=${this._unhidePanel}
|
||||||
|
class="hidden-panel"
|
||||||
|
.panel=${url}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
<ha-icon
|
||||||
|
slot="graphic"
|
||||||
|
.icon=${panel.url_path === this.hass.defaultPanel
|
||||||
|
? "mdi:view-dashboard"
|
||||||
|
: panel.icon}
|
||||||
|
></ha-icon>
|
||||||
|
<span class="item-text"
|
||||||
|
>${panel.url_path === this.hass.defaultPanel
|
||||||
|
? this.hass.localize("panel.states")
|
||||||
|
: this.hass.localize(`panel.${panel.title}`) ||
|
||||||
|
panel.title}</span
|
||||||
|
>
|
||||||
|
<mwc-icon-button class="show-panel">
|
||||||
|
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
</ha-clickable-list-item>`;
|
||||||
|
})}
|
||||||
|
${this._renderSpacer()}`
|
||||||
|
: ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderSpacer(enabled = true) {
|
||||||
|
return enabled
|
||||||
|
? html`<li divider role="separator" class="spacer"></li>`
|
||||||
|
: html`<li divider role="separator" class="bottom-spacer" disabled></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _tooltip() {
|
||||||
|
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _activateEditMode() {
|
||||||
|
if (!Sortable) {
|
||||||
|
const [sortableImport, sortStylesImport] = await Promise.all([
|
||||||
|
import("sortablejs/modular/sortable.core.esm"),
|
||||||
|
import("../resources/ha-sortable-style-ha-clickable"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.innerHTML = sortStylesImport.sortableStyles.cssText;
|
||||||
|
this.shadowRoot!.appendChild(style);
|
||||||
|
|
||||||
|
Sortable = sortableImport.Sortable;
|
||||||
|
Sortable.mount(sortableImport.OnSpill);
|
||||||
|
Sortable.mount(sortableImport.AutoScroll());
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateComplete;
|
||||||
|
|
||||||
|
this._createSortable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createSortable() {
|
||||||
|
this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), {
|
||||||
|
animation: 150,
|
||||||
|
fallbackClass: "sortable-fallback",
|
||||||
|
// fallbackTolerance: 15,
|
||||||
|
dataIdAttr: "data-panel",
|
||||||
|
handle: "ha-clickable-list-item",
|
||||||
|
onSort: async () => {
|
||||||
|
this._panelOrder = this._sortable.toArray();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deactivateEditMode() {
|
||||||
|
this._sortable?.destroy();
|
||||||
|
this._sortable = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _hidePanel(ev: Event) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const panel = (ev.currentTarget as any).panel;
|
||||||
|
if (this._hiddenPanels.includes(panel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Make a copy for Memoize
|
||||||
|
this._hiddenPanels = [...this._hiddenPanels, panel];
|
||||||
|
this._renderEmptySortable = true;
|
||||||
|
await this.updateComplete;
|
||||||
|
const container = this.shadowRoot!.getElementById("sortable")!;
|
||||||
|
while (container.lastElementChild) {
|
||||||
|
container.removeChild(container.lastElementChild);
|
||||||
|
}
|
||||||
|
this._renderEmptySortable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _unhidePanel(ev: Event) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const panel = (ev.currentTarget as any).panel;
|
||||||
|
this._hiddenPanels = this._hiddenPanels.filter(
|
||||||
|
(hidden) => hidden !== panel
|
||||||
|
);
|
||||||
|
this._renderEmptySortable = true;
|
||||||
|
await this.updateComplete;
|
||||||
|
const container = this.shadowRoot!.getElementById("sortable")!;
|
||||||
|
while (container.lastElementChild) {
|
||||||
|
container.removeChild(container.lastElementChild);
|
||||||
|
}
|
||||||
|
this._renderEmptySortable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemMouseEnter(ev: MouseEvent) {
|
||||||
|
// On keypresses on the listbox, we're going to ignore mouse enter events
|
||||||
|
// for 100ms so that we ignore it when pressing down arrow scrolls the
|
||||||
|
// sidebar causing the mouse to hover a new icon
|
||||||
|
if (
|
||||||
|
this.expanded ||
|
||||||
|
new Date().getTime() < this._recentKeydownActiveUntil
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._mouseLeaveTimeout) {
|
||||||
|
clearTimeout(this._mouseLeaveTimeout);
|
||||||
|
this._mouseLeaveTimeout = undefined;
|
||||||
|
}
|
||||||
|
this._showTooltip(ev.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemMouseLeave() {
|
||||||
|
if (this._mouseLeaveTimeout) {
|
||||||
|
clearTimeout(this._mouseLeaveTimeout);
|
||||||
|
}
|
||||||
|
this._mouseLeaveTimeout = window.setTimeout(() => {
|
||||||
|
this._hideTooltip();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listboxFocusIn(ev) {
|
||||||
|
if (this.expanded || ev.target.nodeName !== "A") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._showTooltip(ev.target.querySelector("ha-clickable-list-item"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listboxFocusOut() {
|
||||||
|
this._hideTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
@eventOptions({
|
||||||
|
passive: true,
|
||||||
|
})
|
||||||
|
private _listboxScroll() {
|
||||||
|
// On keypresses on the listbox, we're going to ignore scroll events
|
||||||
|
// for 100ms so that if pressing down arrow scrolls the sidebar, the tooltip
|
||||||
|
// will not be hidden.
|
||||||
|
if (new Date().getTime() < this._recentKeydownActiveUntil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._hideTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listboxKeydown() {
|
||||||
|
this._recentKeydownActiveUntil = new Date().getTime() + 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showTooltip(item) {
|
||||||
|
if (this._tooltipHideTimeout) {
|
||||||
|
clearTimeout(this._tooltipHideTimeout);
|
||||||
|
this._tooltipHideTimeout = undefined;
|
||||||
|
}
|
||||||
|
const tooltip = this._tooltip;
|
||||||
|
const listbox = this.shadowRoot!.querySelector("mwc-list")!;
|
||||||
|
let top = item.offsetTop + 11;
|
||||||
|
if (listbox.contains(item)) {
|
||||||
|
top -= listbox.scrollTop;
|
||||||
|
}
|
||||||
|
tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML;
|
||||||
|
tooltip.style.display = "block";
|
||||||
|
tooltip.style.top = `${top}px`;
|
||||||
|
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hideTooltip() {
|
||||||
|
// Delay it a little in case other events are pending processing.
|
||||||
|
if (!this._tooltipHideTimeout) {
|
||||||
|
this._tooltipHideTimeout = window.setTimeout(() => {
|
||||||
|
this._tooltipHideTimeout = undefined;
|
||||||
|
this._tooltip.style.display = "none";
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleExternalAppConfiguration(ev: Event) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.hass.auth.external!.fireMessage({
|
||||||
|
type: "config_screen/show",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isUtilityPanel(panel: PanelInfo) {
|
||||||
|
return (
|
||||||
|
panel.component_name === "developer-tools" ||
|
||||||
|
panel.component_name === "config" ||
|
||||||
|
panel.component_name === "external-config"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderPanels(panels: PanelInfo[]) {
|
||||||
|
return panels
|
||||||
|
.filter((panel) => !this._isUtilityPanel(panel))
|
||||||
|
.map((panel) =>
|
||||||
|
this._renderPanel(
|
||||||
|
panel.url_path,
|
||||||
|
panel.url_path === this.hass.defaultPanel
|
||||||
|
? panel.title || this.hass.localize("panel.states")
|
||||||
|
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||||
|
panel.icon,
|
||||||
|
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||||
|
? mdiViewDashboard
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderUtilityPanels() {
|
||||||
|
const [, afterSpacer] = computePanels(
|
||||||
|
this.hass.panels,
|
||||||
|
this.hass.defaultPanel,
|
||||||
|
this._panelOrder,
|
||||||
|
this._hiddenPanels
|
||||||
|
);
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
return html`
|
||||||
|
<mwc-list>
|
||||||
|
${afterSpacer.map((panel) =>
|
||||||
|
this._renderPanel(
|
||||||
|
panel.url_path,
|
||||||
|
panel.url_path === this.hass.defaultPanel
|
||||||
|
? panel.title || this.hass.localize("panel.states")
|
||||||
|
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||||
|
panel.icon,
|
||||||
|
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||||
|
? mdiViewDashboard
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
${this._renderExternalConfiguration()}
|
||||||
|
<li divider role="separator" class="spacer"></li>
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderExternalConfiguration() {
|
||||||
|
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||||
|
? html`
|
||||||
|
<ha-clickable-list-item
|
||||||
|
aria-role="option"
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.sidebar.external_app_configuration"
|
||||||
|
)}
|
||||||
|
href="#external-app-configuration"
|
||||||
|
tabindex="-1"
|
||||||
|
@click=${this._handleExternalAppConfiguration}
|
||||||
|
@mouseenter=${this._itemMouseEnter}
|
||||||
|
@mouseleave=${this._itemMouseLeave}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||||
|
<span class="item-text">
|
||||||
|
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||||
|
</span>
|
||||||
|
</ha-clickable-list-item>
|
||||||
|
`
|
||||||
|
: ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderPanel(
|
||||||
|
urlPath: string,
|
||||||
|
title: string | null,
|
||||||
|
icon?: string | null,
|
||||||
|
iconPath?: string | null
|
||||||
|
) {
|
||||||
|
return html`
|
||||||
|
<ha-clickable-list-item
|
||||||
|
.activated=${urlPath === this.hass.panelUrl}
|
||||||
|
.href=${urlPath}
|
||||||
|
data-panel=${urlPath}
|
||||||
|
tabindex="-1"
|
||||||
|
@mouseenter=${this._itemMouseEnter}
|
||||||
|
@mouseleave=${this._itemMouseLeave}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
${iconPath
|
||||||
|
? html`<ha-svg-icon slot="graphic" .path=${iconPath}></ha-svg-icon>`
|
||||||
|
: html`<ha-icon slot="graphic" .icon=${icon}></ha-icon>`}
|
||||||
|
|
||||||
|
<span class="item-text">${title}</span>
|
||||||
|
|
||||||
|
${this.editMode
|
||||||
|
? html`<mwc-icon-button
|
||||||
|
class="hide-panel"
|
||||||
|
.panel=${urlPath}
|
||||||
|
@click=${this._hidePanel}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>`
|
||||||
|
: ""}
|
||||||
|
</ha-clickable-list-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
height: calc(100% + var(--header-height));
|
||||||
|
/* position: absolute; */
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
border-right: 1px solid var(--divider-color);
|
||||||
|
background-color: var(--sidebar-background-color);
|
||||||
|
width: 64px;
|
||||||
|
/* height: calc(100% - var(--header-height) - 132px);
|
||||||
|
height: calc(
|
||||||
|
100% - var(--header-height) - 132px - env(safe-area-inset-bottom)
|
||||||
|
); */
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
width: 256px;
|
||||||
|
width: calc(256px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
|
:host([rtl]) {
|
||||||
|
border-right: 0;
|
||||||
|
border-left: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([rtl]) .menu {
|
||||||
|
padding-left: 8.5px;
|
||||||
|
padding-right: calc(8.5px + env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
:host([expanded]) .menu {
|
||||||
|
width: calc(256px + env(safe-area-inset-left));
|
||||||
|
}
|
||||||
|
:host([rtl][expanded]) .menu {
|
||||||
|
width: calc(256px + env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
.menu mwc-icon-button {
|
||||||
|
color: var(--sidebar-icon-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) .menu mwc-icon-button {
|
||||||
|
margin-right: 23px;
|
||||||
|
}
|
||||||
|
:host([expanded][rtl]) .menu mwc-icon-button {
|
||||||
|
margin-right: 0px;
|
||||||
|
margin-left: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:host([narrow]) .title {
|
||||||
|
padding: 0 0px;
|
||||||
|
}
|
||||||
|
:host([expanded]) .title {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
.title mwc-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#sortable,
|
||||||
|
.hidden-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.panels {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-divider-list-item {
|
||||||
|
/* --ha-divider-height: calc(100vh - var(--header-height)); */
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-list.ha-scrollbar {
|
||||||
|
height: 100%;
|
||||||
|
--mdc-list-vertical-padding: 4px 0;
|
||||||
|
padding: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: none;
|
||||||
|
margin-left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-list.utility-panels {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([rtl]) mwc-list {
|
||||||
|
margin-left: initial;
|
||||||
|
margin-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--sidebar-text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
outline: 0;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-list-item {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 48px;
|
||||||
|
--mdc-list-item-graphic-margin: 16px;
|
||||||
|
--mdc-list-item-meta-size: 32px;
|
||||||
|
}
|
||||||
|
:host([expanded]) mwc-list-item {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
:host([rtl]) mwc-list-item {
|
||||||
|
padding-left: auto;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-icon[slot="graphic"],
|
||||||
|
ha-svg-icon[slot="graphic"] {
|
||||||
|
color: var(--sidebar-icon-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
[slot="graphic"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iron-selected mwc-list-item::before,
|
||||||
|
a:not(.iron-selected):focus::before {
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
transition: opacity 15ms linear;
|
||||||
|
will-change: opacity;
|
||||||
|
}
|
||||||
|
.iron-selected mwc-list-item::before {
|
||||||
|
background-color: var(--sidebar-selected-icon-color);
|
||||||
|
opacity: 0.12;
|
||||||
|
}
|
||||||
|
a:not(.iron-selected):focus::before {
|
||||||
|
background-color: currentColor;
|
||||||
|
opacity: var(--dark-divider-opacity);
|
||||||
|
margin: 4px 8px;
|
||||||
|
}
|
||||||
|
.iron-selected mwc-list-item:focus::before,
|
||||||
|
.iron-selected:focus mwc-list-item::before {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iron-selected mwc-list-item[pressed]:before {
|
||||||
|
opacity: 0.37;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-list-item span {
|
||||||
|
color: var(--sidebar-text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.iron-selected mwc-list-item ha-icon,
|
||||||
|
a.iron-selected mwc-list-item ha-svg-icon {
|
||||||
|
color: var(--sidebar-selected-icon-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.iron-selected .item-text {
|
||||||
|
color: var(--sidebar-selected-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-list-item mwc-list-item .item-text,
|
||||||
|
mwc-list-item .item-text {
|
||||||
|
display: none;
|
||||||
|
max-width: calc(100% - 56px);
|
||||||
|
}
|
||||||
|
:host([expanded]) mwc-list-item .item-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.divider::before {
|
||||||
|
display: block;
|
||||||
|
height: 100px;
|
||||||
|
background-color: var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-spacer {
|
||||||
|
flex: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subheader {
|
||||||
|
color: var(--sidebar-text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-tools {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 8px;
|
||||||
|
width: 256px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-tools a {
|
||||||
|
color: var(--sidebar-icon-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.9;
|
||||||
|
border-radius: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--sidebar-background-color);
|
||||||
|
background-color: var(--sidebar-text-color);
|
||||||
|
padding: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([rtl]) .menu mwc-icon-button {
|
||||||
|
-webkit-transform: scaleX(-1);
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-sidebar-panel-list": HaSidebarPanelList;
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,10 @@
|
|||||||
|
import "./ha-sidebar-panel-list";
|
||||||
import "./ha-clickable-list-item";
|
import "./ha-clickable-list-item";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-icon-button";
|
import "@material/mwc-icon-button";
|
||||||
import {
|
import { mdiBell, mdiMenu, mdiMenuOpen } from "@mdi/js";
|
||||||
mdiBell,
|
|
||||||
mdiCellphoneCog,
|
|
||||||
mdiClose,
|
|
||||||
mdiMenu,
|
|
||||||
mdiMenuOpen,
|
|
||||||
mdiPlus,
|
|
||||||
mdiViewDashboard,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
@ -24,138 +17,25 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { classMap } from "lit-html/directives/class-map";
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
import { guard } from "lit-html/directives/guard";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { LocalStorage } from "../common/decorators/local-storage";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { compare } from "../common/string/compare";
|
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import { ActionHandlerDetail } from "../data/lovelace";
|
import { ActionHandlerDetail } from "../data/lovelace";
|
||||||
import {
|
import {
|
||||||
PersistentNotification,
|
PersistentNotification,
|
||||||
subscribeNotifications,
|
subscribeNotifications,
|
||||||
} from "../data/persistent_notification";
|
} from "../data/persistent_notification";
|
||||||
import {
|
import { getExternalConfig } from "../external_app/external_config";
|
||||||
ExternalConfig,
|
|
||||||
getExternalConfig,
|
|
||||||
} from "../external_app/external_config";
|
|
||||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant, PanelInfo } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-menu-button";
|
import "./ha-menu-button";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./user/ha-user-badge";
|
import "./user/ha-user-badge";
|
||||||
|
|
||||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
|
||||||
|
|
||||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||||
|
|
||||||
const SORT_VALUE_URL_PATHS = {
|
|
||||||
map: 1,
|
|
||||||
logbook: 2,
|
|
||||||
history: 3,
|
|
||||||
"developer-tools": 9,
|
|
||||||
hassio: 10,
|
|
||||||
config: 11,
|
|
||||||
};
|
|
||||||
|
|
||||||
const panelSorter = (
|
|
||||||
reverseSort: string[],
|
|
||||||
defaultPanel: string,
|
|
||||||
a: PanelInfo,
|
|
||||||
b: PanelInfo
|
|
||||||
) => {
|
|
||||||
const indexA = reverseSort.indexOf(a.url_path);
|
|
||||||
const indexB = reverseSort.indexOf(b.url_path);
|
|
||||||
if (indexA !== indexB) {
|
|
||||||
if (indexA < indexB) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return defaultPanelSorter(defaultPanel, a, b);
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultPanelSorter = (
|
|
||||||
defaultPanel: string,
|
|
||||||
a: PanelInfo,
|
|
||||||
b: PanelInfo
|
|
||||||
) => {
|
|
||||||
// Put all the Lovelace at the top.
|
|
||||||
const aLovelace = a.component_name === "lovelace";
|
|
||||||
const bLovelace = b.component_name === "lovelace";
|
|
||||||
|
|
||||||
if (a.url_path === defaultPanel) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (b.url_path === defaultPanel) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aLovelace && bLovelace) {
|
|
||||||
return compare(a.title!, b.title!);
|
|
||||||
}
|
|
||||||
if (aLovelace && !bLovelace) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (bLovelace) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aBuiltIn = a.url_path in SORT_VALUE_URL_PATHS;
|
|
||||||
const bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS;
|
|
||||||
|
|
||||||
if (aBuiltIn && bBuiltIn) {
|
|
||||||
return SORT_VALUE_URL_PATHS[a.url_path] - SORT_VALUE_URL_PATHS[b.url_path];
|
|
||||||
}
|
|
||||||
if (aBuiltIn) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (bBuiltIn) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// both not built in, sort by title
|
|
||||||
return compare(a.title!, b.title!);
|
|
||||||
};
|
|
||||||
|
|
||||||
const computePanels = memoizeOne(
|
|
||||||
(
|
|
||||||
panels: HomeAssistant["panels"],
|
|
||||||
defaultPanel: HomeAssistant["defaultPanel"],
|
|
||||||
panelsOrder: string[],
|
|
||||||
hiddenPanels: string[]
|
|
||||||
): [PanelInfo[], PanelInfo[]] => {
|
|
||||||
if (!panels) {
|
|
||||||
return [[], []];
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeSpacer: PanelInfo[] = [];
|
|
||||||
const afterSpacer: PanelInfo[] = [];
|
|
||||||
|
|
||||||
Object.values(panels).forEach((panel) => {
|
|
||||||
if (
|
|
||||||
hiddenPanels.includes(panel.url_path) ||
|
|
||||||
(!panel.title && panel.url_path !== defaultPanel)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
|
||||||
? afterSpacer
|
|
||||||
: beforeSpacer
|
|
||||||
).push(panel);
|
|
||||||
});
|
|
||||||
|
|
||||||
const reverseSort = [...panelsOrder].reverse();
|
|
||||||
|
|
||||||
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
|
||||||
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
|
|
||||||
|
|
||||||
return [beforeSpacer, afterSpacer];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let Sortable;
|
let Sortable;
|
||||||
|
|
||||||
@customElement("ha-sidebar")
|
@customElement("ha-sidebar")
|
||||||
@ -170,48 +50,54 @@ class HaSidebar extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public editMode = false;
|
@property({ type: Boolean }) public editMode = false;
|
||||||
|
|
||||||
@internalProperty() private _externalConfig?: ExternalConfig;
|
|
||||||
|
|
||||||
@internalProperty() private _notifications?: PersistentNotification[];
|
@internalProperty() private _notifications?: PersistentNotification[];
|
||||||
|
|
||||||
// property used only in css
|
// property used only in css
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||||
|
|
||||||
@internalProperty() private _renderEmptySortable = false;
|
|
||||||
|
|
||||||
private _mouseLeaveTimeout?: number;
|
private _mouseLeaveTimeout?: number;
|
||||||
|
|
||||||
private _tooltipHideTimeout?: number;
|
private _tooltipHideTimeout?: number;
|
||||||
|
|
||||||
private _recentKeydownActiveUntil = 0;
|
private _recentKeydownActiveUntil = 0;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
@LocalStorage("sidebarPanelOrder", true, {
|
|
||||||
attribute: false,
|
|
||||||
})
|
|
||||||
private _panelOrder: string[] = [];
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
@LocalStorage("sidebarHiddenPanels", true, {
|
|
||||||
attribute: false,
|
|
||||||
})
|
|
||||||
private _hiddenPanels: string[] = [];
|
|
||||||
|
|
||||||
private _sortable?;
|
private _sortable?;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
const debug = false;
|
||||||
|
|
||||||
|
return debug
|
||||||
|
? html`
|
||||||
|
<ha-sidebar-panel-list
|
||||||
|
.hass=${this.hass}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
.alwaysExpand=${this.alwaysExpand}
|
||||||
|
></ha-sidebar-panel-list>
|
||||||
|
`
|
||||||
|
: this.render2();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render2() {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return html`
|
return html`
|
||||||
${this._renderHeader()}
|
${this._renderHeader()} ${this._renderAllPanels()}
|
||||||
${this._renderAllPanels()}
|
<mwc-list
|
||||||
${this._renderUtilityPanels()}
|
attr-for-selected="data-panel"
|
||||||
${this._renderNotifications()}
|
@focusin=${this._listboxFocusIn}
|
||||||
${this._renderUserItem()}
|
@focusout=${this._listboxFocusOut}
|
||||||
|
@scroll=${this._listboxScroll}
|
||||||
|
@keydown=${this._listboxKeydown}
|
||||||
|
>
|
||||||
|
${this._renderNotifications()} ${this._renderUserItem()}
|
||||||
|
</mwc-list>
|
||||||
${this._renderSpacer()}
|
${this._renderSpacer()}
|
||||||
<div class="tooltip"></div>
|
<div class="tooltip"></div>
|
||||||
`;
|
`;
|
||||||
@ -254,9 +140,7 @@ class HaSidebar extends LitElement {
|
|||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
if (this.hass && this.hass.auth.external) {
|
if (this.hass && this.hass.auth.external) {
|
||||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
getExternalConfig(this.hass.auth.external).then(() => {});
|
||||||
this._externalConfig = conf;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||||
this._notifications = notifications;
|
this._notifications = notifications;
|
||||||
@ -328,80 +212,19 @@ class HaSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderAllPanels() {
|
private _renderAllPanels() {
|
||||||
const [beforeSpacer, afterSpacer] = computePanels(
|
|
||||||
this.hass.panels,
|
|
||||||
this.hass.defaultPanel,
|
|
||||||
this._panelOrder,
|
|
||||||
this._hiddenPanels
|
|
||||||
);
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
return html`
|
return html`
|
||||||
<mwc-list
|
<ha-sidebar-panel-list
|
||||||
attr-for-selected="data-panel"
|
.hass=${this.hass}
|
||||||
class="ha-scrollbar"
|
.expanded=${this.expanded}
|
||||||
@focusin=${this._listboxFocusIn}
|
.alwaysExpand=${this.alwaysExpand}
|
||||||
@focusout=${this._listboxFocusOut}
|
></ha-sidebar-panel-list>
|
||||||
@scroll=${this._listboxScroll}
|
|
||||||
@keydown=${this._listboxKeydown}
|
|
||||||
>
|
|
||||||
${this.editMode
|
|
||||||
? this._renderPanelsEdit(beforeSpacer)
|
|
||||||
: this._renderPanels(beforeSpacer)}
|
|
||||||
${this._renderSpacer()}
|
|
||||||
${this._renderPanels(afterSpacer)}
|
|
||||||
${this._renderExternalConfiguration()}
|
|
||||||
</mwc-list>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
private _renderSpacer(enabled = true) {
|
||||||
// prettier-ignore
|
return enabled
|
||||||
return html`<div id="sortable">
|
? html`<li divider role="separator" class="spacer"></li>`
|
||||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
: html`<li divider role="separator" class="bottom-spacer" disabled></li>`;
|
||||||
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
${this._renderSpacer()}
|
|
||||||
${this._renderHiddenPanels()} `;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderHiddenPanels() {
|
|
||||||
return html` ${this._hiddenPanels.length
|
|
||||||
? html`${this._hiddenPanels.map((url) => {
|
|
||||||
const panel = this.hass.panels[url];
|
|
||||||
if (!panel) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return html`<ha-clickable-list-item
|
|
||||||
@click=${this._unhidePanel}
|
|
||||||
class="hidden-panel"
|
|
||||||
.panel=${url}
|
|
||||||
graphic="icon"
|
|
||||||
>
|
|
||||||
<ha-icon
|
|
||||||
slot="graphic"
|
|
||||||
.icon=${panel.url_path === this.hass.defaultPanel
|
|
||||||
? "mdi:view-dashboard"
|
|
||||||
: panel.icon}
|
|
||||||
></ha-icon>
|
|
||||||
<span class="item-text"
|
|
||||||
>${panel.url_path === this.hass.defaultPanel
|
|
||||||
? this.hass.localize("panel.states")
|
|
||||||
: this.hass.localize(`panel.${panel.title}`) ||
|
|
||||||
panel.title}</span
|
|
||||||
>
|
|
||||||
<mwc-icon-button class="show-panel">
|
|
||||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
|
||||||
</mwc-icon-button>
|
|
||||||
</ha-clickable-list-item>`;
|
|
||||||
})}
|
|
||||||
${this._renderSpacer()}`
|
|
||||||
: ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderSpacer() {
|
|
||||||
return html`<li divider role="separator" class="spacer"></li>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderNotifications() {
|
private _renderNotifications() {
|
||||||
@ -473,30 +296,6 @@ class HaSidebar extends LitElement {
|
|||||||
</ha-clickable-list-item> `;
|
</ha-clickable-list-item> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderExternalConfiguration() {
|
|
||||||
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
|
||||||
? html`
|
|
||||||
<ha-clickable-list-item
|
|
||||||
aria-role="option"
|
|
||||||
aria-label=${this.hass.localize(
|
|
||||||
"ui.sidebar.external_app_configuration"
|
|
||||||
)}
|
|
||||||
href="#external-app-configuration"
|
|
||||||
tabindex="-1"
|
|
||||||
@click=${this._handleExternalAppConfiguration}
|
|
||||||
@mouseenter=${this._itemMouseEnter}
|
|
||||||
@mouseleave=${this._itemMouseLeave}
|
|
||||||
graphic="icon"
|
|
||||||
>
|
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiCellphoneCog}></ha-svg-icon>
|
|
||||||
<span class="item-text">
|
|
||||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
|
||||||
</span>
|
|
||||||
</ha-clickable-list-item>
|
|
||||||
`
|
|
||||||
: ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _tooltip() {
|
private get _tooltip() {
|
||||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||||
}
|
}
|
||||||
@ -537,9 +336,7 @@ class HaSidebar extends LitElement {
|
|||||||
// fallbackTolerance: 15,
|
// fallbackTolerance: 15,
|
||||||
dataIdAttr: "data-panel",
|
dataIdAttr: "data-panel",
|
||||||
handle: "ha-clickable-list-item",
|
handle: "ha-clickable-list-item",
|
||||||
onSort: async () => {
|
onSort: async () => {},
|
||||||
this._panelOrder = this._sortable.toArray();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,38 +349,6 @@ class HaSidebar extends LitElement {
|
|||||||
fireEvent(this, "hass-edit-sidebar", { editMode: false });
|
fireEvent(this, "hass-edit-sidebar", { editMode: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _hidePanel(ev: Event) {
|
|
||||||
ev.preventDefault();
|
|
||||||
const panel = (ev.currentTarget as any).panel;
|
|
||||||
if (this._hiddenPanels.includes(panel)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Make a copy for Memoize
|
|
||||||
this._hiddenPanels = [...this._hiddenPanels, panel];
|
|
||||||
this._renderEmptySortable = true;
|
|
||||||
await this.updateComplete;
|
|
||||||
const container = this.shadowRoot!.getElementById("sortable")!;
|
|
||||||
while (container.lastElementChild) {
|
|
||||||
container.removeChild(container.lastElementChild);
|
|
||||||
}
|
|
||||||
this._renderEmptySortable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _unhidePanel(ev: Event) {
|
|
||||||
ev.preventDefault();
|
|
||||||
const panel = (ev.currentTarget as any).panel;
|
|
||||||
this._hiddenPanels = this._hiddenPanels.filter(
|
|
||||||
(hidden) => hidden !== panel
|
|
||||||
);
|
|
||||||
this._renderEmptySortable = true;
|
|
||||||
await this.updateComplete;
|
|
||||||
const container = this.shadowRoot!.getElementById("sortable")!;
|
|
||||||
while (container.lastElementChild) {
|
|
||||||
container.removeChild(container.lastElementChild);
|
|
||||||
}
|
|
||||||
this._renderEmptySortable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _itemMouseEnter(ev: MouseEvent) {
|
private _itemMouseEnter(ev: MouseEvent) {
|
||||||
// On keypresses on the listbox, we're going to ignore mouse enter events
|
// On keypresses on the listbox, we're going to ignore mouse enter events
|
||||||
// for 100ms so that we ignore it when pressing down arrow scrolls the
|
// for 100ms so that we ignore it when pressing down arrow scrolls the
|
||||||
@ -669,13 +434,6 @@ class HaSidebar extends LitElement {
|
|||||||
fireEvent(this, "hass-show-notifications");
|
fireEvent(this, "hass-show-notifications");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleExternalAppConfiguration(ev: Event) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.hass.auth.external!.fireMessage({
|
|
||||||
type: "config_screen/show",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleSidebar(ev: CustomEvent) {
|
private _toggleSidebar(ev: CustomEvent) {
|
||||||
if (ev.detail.action !== "tap") {
|
if (ev.detail.action !== "tap") {
|
||||||
return;
|
return;
|
||||||
@ -683,118 +441,12 @@ class HaSidebar extends LitElement {
|
|||||||
fireEvent(this, "hass-toggle-menu");
|
fireEvent(this, "hass-toggle-menu");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isUtilityPanel(panel: PanelInfo) {
|
|
||||||
return (
|
|
||||||
panel.component_name === "developer-tools" ||
|
|
||||||
panel.component_name === "config" ||
|
|
||||||
panel.component_name === "external-config"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderPanels(panels: PanelInfo[]) {
|
|
||||||
return panels
|
|
||||||
.filter((panel) => !this._isUtilityPanel(panel))
|
|
||||||
.map((panel) =>
|
|
||||||
this._renderPanel(
|
|
||||||
panel.url_path,
|
|
||||||
panel.url_path === this.hass.defaultPanel
|
|
||||||
? panel.title || this.hass.localize("panel.states")
|
|
||||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
|
||||||
panel.icon,
|
|
||||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
|
||||||
? mdiViewDashboard
|
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderUtilityPanels() {
|
|
||||||
const [, afterSpacer] = computePanels(
|
|
||||||
this.hass.panels,
|
|
||||||
this.hass.defaultPanel,
|
|
||||||
this._panelOrder,
|
|
||||||
this._hiddenPanels
|
|
||||||
);
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
return html`
|
|
||||||
<mwc-list
|
|
||||||
attr-for-selected="data-panel"
|
|
||||||
@focusin=${this._listboxFocusIn}
|
|
||||||
@focusout=${this._listboxFocusOut}
|
|
||||||
@scroll=${this._listboxScroll}
|
|
||||||
@keydown=${this._listboxKeydown}
|
|
||||||
>
|
|
||||||
${afterSpacer.map((panel) =>
|
|
||||||
this._renderPanel(
|
|
||||||
panel.url_path,
|
|
||||||
panel.url_path === this.hass.defaultPanel
|
|
||||||
? panel.title || this.hass.localize("panel.states")
|
|
||||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
|
||||||
panel.icon,
|
|
||||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
|
||||||
? mdiViewDashboard
|
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
${afterSpacer.map((panel) =>
|
|
||||||
this._renderPanel(
|
|
||||||
panel.url_path,
|
|
||||||
panel.url_path === this.hass.defaultPanel
|
|
||||||
? panel.title || this.hass.localize("panel.states")
|
|
||||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
|
||||||
panel.icon,
|
|
||||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
|
||||||
? mdiViewDashboard
|
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<li divider role="separator" class="spacer"></li>
|
|
||||||
</mwc-list>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderPanel(
|
|
||||||
urlPath: string,
|
|
||||||
title: string | null,
|
|
||||||
icon?: string | null,
|
|
||||||
iconPath?: string | null
|
|
||||||
) {
|
|
||||||
return html`
|
|
||||||
<ha-clickable-list-item
|
|
||||||
.activated=${urlPath === this.hass.panelUrl}
|
|
||||||
.href=${urlPath}
|
|
||||||
data-panel=${urlPath}
|
|
||||||
tabindex="-1"
|
|
||||||
@mouseenter=${this._itemMouseEnter}
|
|
||||||
@mouseleave=${this._itemMouseLeave}
|
|
||||||
graphic="icon"
|
|
||||||
>
|
|
||||||
${iconPath
|
|
||||||
? html`<ha-svg-icon slot="graphic" .path=${iconPath}></ha-svg-icon>`
|
|
||||||
: html`<ha-icon slot="graphic" .icon=${icon}></ha-icon>`}
|
|
||||||
|
|
||||||
<span class="item-text">${title}</span>
|
|
||||||
|
|
||||||
${this.editMode
|
|
||||||
? html`<mwc-icon-button
|
|
||||||
class="hide-panel"
|
|
||||||
.panel=${urlPath}
|
|
||||||
@click=${this._hidePanel}
|
|
||||||
>
|
|
||||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
|
||||||
</mwc-icon-button>`
|
|
||||||
: ""}
|
|
||||||
</ha-clickable-list-item>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
height: 100%;
|
height: calc(100% - var(--header-height));
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
@ -869,7 +521,8 @@ class HaSidebar extends LitElement {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-list.ha-scrollbar {
|
/* mwc-list.ha-scrollbar {
|
||||||
|
height: 100%;
|
||||||
--mdc-list-vertical-padding: 4px 0;
|
--mdc-list-vertical-padding: 4px 0;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -882,7 +535,8 @@ class HaSidebar extends LitElement {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: none;
|
background: none;
|
||||||
margin-left: env(safe-area-inset-left);
|
margin-left: env(safe-area-inset-left);
|
||||||
}
|
background-color: blue;
|
||||||
|
} */
|
||||||
|
|
||||||
:host([rtl]) mwc-list {
|
:host([rtl]) mwc-list {
|
||||||
margin-left: initial;
|
margin-left: initial;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user