mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Allow to move and hide sidebar items (#6755)
This commit is contained in:
parent
5292119e6e
commit
3bd2e8dbf5
@ -114,6 +114,7 @@
|
|||||||
"regenerator-runtime": "^0.13.2",
|
"regenerator-runtime": "^0.13.2",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
|
"sortablejs": "^1.10.2",
|
||||||
"superstruct": "^0.10.12",
|
"superstruct": "^0.10.12",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
|
68
src/common/decorators/local-storage.ts
Normal file
68
src/common/decorators/local-storage.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { ClassElement } from "../../types";
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
private _storage: any = {};
|
||||||
|
|
||||||
|
public addFromStorage(storageKey: any): void {
|
||||||
|
if (!this._storage[storageKey]) {
|
||||||
|
const data = window.localStorage.getItem(storageKey);
|
||||||
|
if (data) {
|
||||||
|
this._storage[storageKey] = JSON.parse(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasKey(storageKey: string): any {
|
||||||
|
return storageKey in this._storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getValue(storageKey: string): any {
|
||||||
|
return this._storage[storageKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setValue(storageKey: string, value: any): any {
|
||||||
|
this._storage[storageKey] = value;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKey, JSON.stringify(value));
|
||||||
|
} catch (err) {
|
||||||
|
// Safari in private mode doesn't allow localstorage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = new Storage();
|
||||||
|
|
||||||
|
export const LocalStorage = (key?: string) => {
|
||||||
|
return (element: ClassElement, propName: string) => {
|
||||||
|
const storageKey = key || propName;
|
||||||
|
const initVal = element.initializer ? element.initializer() : undefined;
|
||||||
|
|
||||||
|
storage.addFromStorage(storageKey);
|
||||||
|
|
||||||
|
const getValue = (): any => {
|
||||||
|
return storage.hasKey(storageKey)
|
||||||
|
? storage.getValue(storageKey)
|
||||||
|
: initVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (val: any) => {
|
||||||
|
storage.setValue(storageKey, val);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "method",
|
||||||
|
placement: "own",
|
||||||
|
key: element.key,
|
||||||
|
descriptor: {
|
||||||
|
set(value) {
|
||||||
|
setValue(value);
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return getValue();
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
77
src/components/ha-sidebar-sort-styles.ts
Normal file
77
src/components/ha-sidebar-sort-styles.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { html } from "lit-element";
|
||||||
|
|
||||||
|
export const sortStyles = html`
|
||||||
|
<style>
|
||||||
|
#sortable a:nth-of-type(2n) paper-icon-item {
|
||||||
|
animation-name: keyframes1;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
transform-origin: 50% 10%;
|
||||||
|
animation-delay: -0.75s;
|
||||||
|
animation-duration: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sortable a:nth-of-type(2n-1) paper-icon-item {
|
||||||
|
animation-name: keyframes2;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
transform-origin: 30% 5%;
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
animation-duration: 0.33s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sortable {
|
||||||
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-fallback {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes keyframes1 {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-1deg);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(1.5deg);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes keyframes2 {
|
||||||
|
0% {
|
||||||
|
transform: rotate(1deg);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(-1.5deg);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-panel {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([expanded]) .hide-panel {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
paper-icon-item.hidden-panel,
|
||||||
|
paper-icon-item.hidden-panel span,
|
||||||
|
paper-icon-item.hidden-panel ha-icon[slot="item-icon"] {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
@ -1,9 +1,12 @@
|
|||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-icon-button";
|
import "@material/mwc-icon-button";
|
||||||
import {
|
import {
|
||||||
mdiBell,
|
mdiBell,
|
||||||
mdiCellphoneCog,
|
mdiCellphoneCog,
|
||||||
mdiMenuOpen,
|
mdiClose,
|
||||||
mdiMenu,
|
mdiMenu,
|
||||||
|
mdiMenuOpen,
|
||||||
|
mdiPlus,
|
||||||
mdiViewDashboard,
|
mdiViewDashboard,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import "@polymer/paper-item/paper-icon-item";
|
import "@polymer/paper-item/paper-icon-item";
|
||||||
@ -13,20 +16,24 @@ import "@polymer/paper-listbox/paper-listbox";
|
|||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
|
customElement,
|
||||||
eventOptions,
|
eventOptions,
|
||||||
html,
|
html,
|
||||||
customElement,
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
} 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 { compare } from "../common/string/compare";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import { getDefaultPanel } from "../data/panel";
|
import { ActionHandlerDetail } from "../data/lovelace";
|
||||||
import {
|
import {
|
||||||
PersistentNotification,
|
PersistentNotification,
|
||||||
subscribeNotifications,
|
subscribeNotifications,
|
||||||
@ -35,6 +42,7 @@ import {
|
|||||||
ExternalConfig,
|
ExternalConfig,
|
||||||
getExternalConfig,
|
getExternalConfig,
|
||||||
} from "../external_app/external_config";
|
} from "../external_app/external_config";
|
||||||
|
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||||
import type { HomeAssistant, PanelInfo } from "../types";
|
import type { HomeAssistant, PanelInfo } from "../types";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-menu-button";
|
import "./ha-menu-button";
|
||||||
@ -54,11 +62,39 @@ const SORT_VALUE_URL_PATHS = {
|
|||||||
config: 11,
|
config: 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
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.
|
// Put all the Lovelace at the top.
|
||||||
const aLovelace = a.component_name === "lovelace";
|
const aLovelace = a.component_name === "lovelace";
|
||||||
const bLovelace = b.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) {
|
if (aLovelace && bLovelace) {
|
||||||
return compare(a.title!, b.title!);
|
return compare(a.title!, b.title!);
|
||||||
}
|
}
|
||||||
@ -85,30 +121,45 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
|||||||
return compare(a.title!, b.title!);
|
return compare(a.title!, b.title!);
|
||||||
};
|
};
|
||||||
|
|
||||||
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
const computePanels = memoizeOne(
|
||||||
const panels = hass.panels;
|
(
|
||||||
if (!panels) {
|
panels: HomeAssistant["panels"],
|
||||||
return [[], []];
|
defaultPanel: HomeAssistant["defaultPanel"],
|
||||||
}
|
panelsOrder: string[],
|
||||||
|
hiddenPanels: string[]
|
||||||
const beforeSpacer: PanelInfo[] = [];
|
): [PanelInfo[], PanelInfo[]] => {
|
||||||
const afterSpacer: PanelInfo[] = [];
|
if (!panels) {
|
||||||
|
return [[], []];
|
||||||
Object.values(panels).forEach((panel) => {
|
|
||||||
if (!panel.title || panel.url_path === hass.defaultPanel) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
|
||||||
? afterSpacer
|
|
||||||
: beforeSpacer
|
|
||||||
).push(panel);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeSpacer.sort(panelSorter);
|
const beforeSpacer: PanelInfo[] = [];
|
||||||
afterSpacer.sort(panelSorter);
|
const afterSpacer: PanelInfo[] = [];
|
||||||
|
|
||||||
return [beforeSpacer, afterSpacer];
|
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 sortStyles: TemplateResult;
|
||||||
|
|
||||||
@customElement("ha-sidebar")
|
@customElement("ha-sidebar")
|
||||||
class HaSidebar extends LitElement {
|
class HaSidebar extends LitElement {
|
||||||
@ -124,16 +175,30 @@ class HaSidebar extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() private _notifications?: PersistentNotification[];
|
@internalProperty() private _notifications?: PersistentNotification[];
|
||||||
|
|
||||||
|
@internalProperty() private _editMode = false;
|
||||||
|
|
||||||
// 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")
|
||||||
|
private _panelOrder: string[] = [];
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
@LocalStorage("sidebarHiddenPanels")
|
||||||
|
private _hiddenPanels: string[] = [];
|
||||||
|
|
||||||
|
private _sortable?;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const hass = this.hass;
|
const hass = this.hass;
|
||||||
|
|
||||||
@ -141,7 +206,12 @@ class HaSidebar extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [beforeSpacer, afterSpacer] = computePanels(hass);
|
const [beforeSpacer, afterSpacer] = computePanels(
|
||||||
|
hass.panels,
|
||||||
|
hass.defaultPanel,
|
||||||
|
this._panelOrder,
|
||||||
|
this._hiddenPanels
|
||||||
|
);
|
||||||
|
|
||||||
let notificationCount = this._notifications
|
let notificationCount = this._notifications
|
||||||
? this._notifications.length
|
? this._notifications.length
|
||||||
@ -152,9 +222,8 @@ class HaSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPanel = getDefaultPanel(hass);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
${this._editMode ? sortStyles : ""}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`
|
? html`
|
||||||
@ -170,7 +239,13 @@ class HaSidebar extends LitElement {
|
|||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
<span class="title">Home Assistant</span>
|
<div class="title">
|
||||||
|
${this._editMode
|
||||||
|
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||||
|
DONE
|
||||||
|
</mwc-button>`
|
||||||
|
: "Home Assistant"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<paper-listbox
|
<paper-listbox
|
||||||
attr-for-selected="data-panel"
|
attr-for-selected="data-panel"
|
||||||
@ -179,31 +254,53 @@ class HaSidebar extends LitElement {
|
|||||||
@focusout=${this._listboxFocusOut}
|
@focusout=${this._listboxFocusOut}
|
||||||
@scroll=${this._listboxScroll}
|
@scroll=${this._listboxScroll}
|
||||||
@keydown=${this._listboxKeydown}
|
@keydown=${this._listboxKeydown}
|
||||||
|
@action=${this._handleAction}
|
||||||
|
.actionHandler=${actionHandler({
|
||||||
|
hasHold: !this._editMode,
|
||||||
|
disabled: this._editMode,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
${this._renderPanel(
|
${this._editMode
|
||||||
defaultPanel.url_path,
|
? html`<div id="sortable">
|
||||||
defaultPanel.title || hass.localize("panel.states"),
|
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||||
defaultPanel.icon,
|
this._renderEmptySortable
|
||||||
!defaultPanel.icon ? mdiViewDashboard : undefined
|
? ""
|
||||||
)}
|
: this._renderPanels(beforeSpacer)
|
||||||
${beforeSpacer.map((panel) =>
|
)}
|
||||||
this._renderPanel(
|
</div>`
|
||||||
panel.url_path,
|
: this._renderPanels(beforeSpacer)}
|
||||||
hass.localize(`panel.${panel.title}`) || panel.title,
|
|
||||||
panel.icon,
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div class="spacer" disabled></div>
|
<div class="spacer" disabled></div>
|
||||||
|
${this._editMode && this._hiddenPanels.length
|
||||||
${afterSpacer.map((panel) =>
|
? html`
|
||||||
this._renderPanel(
|
${this._hiddenPanels.map((url) => {
|
||||||
panel.url_path,
|
const panel = this.hass.panels[url];
|
||||||
hass.localize(`panel.${panel.title}`) || panel.title,
|
return html`<paper-icon-item
|
||||||
panel.icon,
|
@click=${this._unhidePanel}
|
||||||
undefined
|
class="hidden-panel"
|
||||||
)
|
>
|
||||||
)}
|
<ha-icon
|
||||||
|
slot="item-icon"
|
||||||
|
.icon=${panel.url_path === "lovelace"
|
||||||
|
? "mdi:view-dashboard"
|
||||||
|
: panel.icon}
|
||||||
|
></ha-icon>
|
||||||
|
<span class="item-text"
|
||||||
|
>${panel.url_path === "lovelace"
|
||||||
|
? hass.localize("panel.states")
|
||||||
|
: hass.localize(`panel.${panel.title}`) ||
|
||||||
|
panel.title}</span
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
class="hide-panel"
|
||||||
|
.panel=${url}
|
||||||
|
.path=${mdiPlus}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</paper-icon-item>`;
|
||||||
|
})}
|
||||||
|
<div class="spacer" disabled></div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._renderPanels(afterSpacer)}
|
||||||
${this._externalConfig && this._externalConfig.hasSettingsScreen
|
${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||||
? html`
|
? html`
|
||||||
<a
|
<a
|
||||||
@ -295,7 +392,9 @@ class HaSidebar extends LitElement {
|
|||||||
changedProps.has("narrow") ||
|
changedProps.has("narrow") ||
|
||||||
changedProps.has("alwaysExpand") ||
|
changedProps.has("alwaysExpand") ||
|
||||||
changedProps.has("_externalConfig") ||
|
changedProps.has("_externalConfig") ||
|
||||||
changedProps.has("_notifications")
|
changedProps.has("_notifications") ||
|
||||||
|
changedProps.has("_editMode") ||
|
||||||
|
changedProps.has("_renderEmptySortable")
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -361,6 +460,74 @@ class HaSidebar extends LitElement {
|
|||||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
|
||||||
|
if (ev.detail.action !== "hold") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Sortable) {
|
||||||
|
const [sortableImport, sortStylesImport] = await Promise.all([
|
||||||
|
import("sortablejs/modular/sortable.core.esm"),
|
||||||
|
import("./ha-sidebar-sort-styles"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
sortStyles = sortStylesImport.sortStyles;
|
||||||
|
|
||||||
|
Sortable = sortableImport.Sortable;
|
||||||
|
Sortable.mount(sortableImport.OnSpill);
|
||||||
|
Sortable.mount(sortableImport.AutoScroll());
|
||||||
|
}
|
||||||
|
this._editMode = true;
|
||||||
|
|
||||||
|
await this.updateComplete;
|
||||||
|
|
||||||
|
this._createSortable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createSortable() {
|
||||||
|
this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), {
|
||||||
|
animation: 150,
|
||||||
|
fallbackClass: "sortable-fallback",
|
||||||
|
dataIdAttr: "data-panel",
|
||||||
|
onSort: async () => {
|
||||||
|
this._panelOrder = this._sortable.toArray();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closeEditMode() {
|
||||||
|
this._sortable?.destroy();
|
||||||
|
this._sortable = undefined;
|
||||||
|
this._editMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _hidePanel(ev: Event) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const panel = (ev.target 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;
|
||||||
|
this._renderEmptySortable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _unhidePanel(ev: Event) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const index = this._hiddenPanels.indexOf((ev.target as any).panel);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._hiddenPanels.splice(index, 1);
|
||||||
|
// Make a copy for Memoize
|
||||||
|
this._hiddenPanels = [...this._hiddenPanels];
|
||||||
|
this._renderEmptySortable = true;
|
||||||
|
await this.updateComplete;
|
||||||
|
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
|
||||||
@ -457,6 +624,19 @@ class HaSidebar extends LitElement {
|
|||||||
fireEvent(this, "hass-toggle-menu");
|
fireEvent(this, "hass-toggle-menu");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _renderPanels(panels: PanelInfo[]) {
|
||||||
|
return panels.map((panel) =>
|
||||||
|
this._renderPanel(
|
||||||
|
panel.url_path,
|
||||||
|
panel.url_path === "lovelace"
|
||||||
|
? this.hass.localize("panel.states")
|
||||||
|
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||||
|
panel.url_path === "lovelace" ? undefined : panel.icon,
|
||||||
|
panel.url_path === "lovelace" ? mdiViewDashboard : undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private _renderPanel(
|
private _renderPanel(
|
||||||
urlPath: string,
|
urlPath: string,
|
||||||
title: string | null,
|
title: string | null,
|
||||||
@ -480,6 +660,14 @@ class HaSidebar extends LitElement {
|
|||||||
></ha-svg-icon>`
|
></ha-svg-icon>`
|
||||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||||
<span class="item-text">${title}</span>
|
<span class="item-text">${title}</span>
|
||||||
|
${this._editMode
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="hide-panel"
|
||||||
|
.panel=${urlPath}
|
||||||
|
@click=${this._hidePanel}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: ""}
|
||||||
</paper-icon-item>
|
</paper-icon-item>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
@ -542,11 +730,15 @@ class HaSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
width: 100%;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
:host([expanded]) .title {
|
:host([expanded]) .title {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
.title mwc-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
paper-listbox::-webkit-scrollbar {
|
paper-listbox::-webkit-scrollbar {
|
||||||
width: 0.4rem;
|
width: 0.4rem;
|
||||||
|
@ -318,10 +318,11 @@ export interface WindowWithLovelaceProm extends Window {
|
|||||||
export interface ActionHandlerOptions {
|
export interface ActionHandlerOptions {
|
||||||
hasHold?: boolean;
|
hasHold?: boolean;
|
||||||
hasDoubleClick?: boolean;
|
hasDoubleClick?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionHandlerDetail {
|
export interface ActionHandlerDetail {
|
||||||
action: string;
|
action: "hold" | "tap" | "double_tap";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;
|
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;
|
||||||
|
@ -2,6 +2,7 @@ import "@material/mwc-ripple";
|
|||||||
import type { Ripple } from "@material/mwc-ripple";
|
import type { Ripple } from "@material/mwc-ripple";
|
||||||
import { directive, PropertyPart } from "lit-html";
|
import { directive, PropertyPart } from "lit-html";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { deepEqual } from "../../../../common/util/deep-equal";
|
||||||
import {
|
import {
|
||||||
ActionHandlerDetail,
|
ActionHandlerDetail,
|
||||||
ActionHandlerOptions,
|
ActionHandlerOptions,
|
||||||
@ -17,10 +18,18 @@ interface ActionHandler extends HTMLElement {
|
|||||||
bind(element: Element, options): void;
|
bind(element: Element, options): void;
|
||||||
}
|
}
|
||||||
interface ActionHandlerElement extends HTMLElement {
|
interface ActionHandlerElement extends HTMLElement {
|
||||||
actionHandler?: boolean;
|
actionHandler?: {
|
||||||
|
options: ActionHandlerOptions;
|
||||||
|
start?: (ev: Event) => void;
|
||||||
|
end?: (ev: Event) => void;
|
||||||
|
handleEnter?: (ev: KeyboardEvent) => void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"action-handler": ActionHandler;
|
||||||
|
}
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
action: ActionHandlerDetail;
|
action: ActionHandlerDetail;
|
||||||
}
|
}
|
||||||
@ -76,26 +85,45 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public bind(element: ActionHandlerElement, options) {
|
public bind(element: ActionHandlerElement, options: ActionHandlerOptions) {
|
||||||
if (element.actionHandler) {
|
if (
|
||||||
|
element.actionHandler &&
|
||||||
|
deepEqual(options, element.actionHandler.options)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
element.actionHandler = true;
|
|
||||||
|
|
||||||
element.addEventListener("contextmenu", (ev: Event) => {
|
if (element.actionHandler) {
|
||||||
const e = ev || window.event;
|
element.removeEventListener("touchstart", element.actionHandler.start!);
|
||||||
if (e.preventDefault) {
|
element.removeEventListener("touchend", element.actionHandler.end!);
|
||||||
e.preventDefault();
|
element.removeEventListener("touchcancel", element.actionHandler.end!);
|
||||||
}
|
|
||||||
if (e.stopPropagation) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
e.cancelBubble = true;
|
|
||||||
e.returnValue = false;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const start = (ev: Event) => {
|
element.removeEventListener("mousedown", element.actionHandler.start!);
|
||||||
|
element.removeEventListener("click", element.actionHandler.end!);
|
||||||
|
|
||||||
|
element.removeEventListener("keyup", element.actionHandler.handleEnter!);
|
||||||
|
} else {
|
||||||
|
element.addEventListener("contextmenu", (ev: Event) => {
|
||||||
|
const e = ev || window.event;
|
||||||
|
if (e.preventDefault) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (e.stopPropagation) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
e.cancelBubble = true;
|
||||||
|
e.returnValue = false;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
element.actionHandler = { options };
|
||||||
|
|
||||||
|
if (options.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.actionHandler.start = (ev: Event) => {
|
||||||
this.held = false;
|
this.held = false;
|
||||||
let x;
|
let x;
|
||||||
let y;
|
let y;
|
||||||
@ -107,13 +135,19 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
y = (ev as MouseEvent).pageY;
|
y = (ev as MouseEvent).pageY;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timer = window.setTimeout(() => {
|
if (options.hasHold) {
|
||||||
this.startAnimation(x, y);
|
this.timer = window.setTimeout(() => {
|
||||||
this.held = true;
|
this.startAnimation(x, y);
|
||||||
}, this.holdTime);
|
this.held = true;
|
||||||
|
}, this.holdTime);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const end = (ev: Event) => {
|
element.actionHandler.end = (ev: Event) => {
|
||||||
|
// Don't respond on our own generated click
|
||||||
|
if (!ev.isTrusted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Prevent mouse event if touch event
|
// Prevent mouse event if touch event
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (
|
if (
|
||||||
@ -122,9 +156,11 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearTimeout(this.timer);
|
if (options.hasHold) {
|
||||||
this.stopAnimation();
|
clearTimeout(this.timer);
|
||||||
this.timer = undefined;
|
this.stopAnimation();
|
||||||
|
this.timer = undefined;
|
||||||
|
}
|
||||||
if (this.held) {
|
if (this.held) {
|
||||||
fireEvent(element, "action", { action: "hold" });
|
fireEvent(element, "action", { action: "hold" });
|
||||||
} else if (options.hasDoubleClick) {
|
} else if (options.hasDoubleClick) {
|
||||||
@ -143,24 +179,30 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fireEvent(element, "action", { action: "tap" });
|
fireEvent(element, "action", { action: "tap" });
|
||||||
|
// Fire the click we prevented the action for
|
||||||
|
(ev.target as HTMLElement)?.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnter = (ev: KeyboardEvent) => {
|
element.actionHandler.handleEnter = (ev: KeyboardEvent) => {
|
||||||
if (ev.keyCode !== 13) {
|
if (ev.keyCode !== 13) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
end(ev);
|
(ev.currentTarget as ActionHandlerElement).actionHandler!.end!(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
element.addEventListener("touchstart", start, { passive: true });
|
element.addEventListener("touchstart", element.actionHandler.start, {
|
||||||
element.addEventListener("touchend", end);
|
passive: true,
|
||||||
element.addEventListener("touchcancel", end);
|
});
|
||||||
|
element.addEventListener("touchend", element.actionHandler.end);
|
||||||
|
element.addEventListener("touchcancel", element.actionHandler.end);
|
||||||
|
|
||||||
element.addEventListener("mousedown", start, { passive: true });
|
element.addEventListener("mousedown", element.actionHandler.start, {
|
||||||
element.addEventListener("click", end);
|
passive: true,
|
||||||
|
});
|
||||||
|
element.addEventListener("click", element.actionHandler.end);
|
||||||
|
|
||||||
element.addEventListener("keyup", handleEnter);
|
element.addEventListener("keyup", element.actionHandler.handleEnter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private startAnimation(x: number, y: number) {
|
private startAnimation(x: number, y: number) {
|
||||||
|
@ -10975,6 +10975,11 @@ sockjs@0.3.19:
|
|||||||
faye-websocket "^0.10.0"
|
faye-websocket "^0.10.0"
|
||||||
uuid "^3.0.1"
|
uuid "^3.0.1"
|
||||||
|
|
||||||
|
sortablejs@^1.10.2:
|
||||||
|
version "1.10.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
|
||||||
|
integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
|
||||||
|
|
||||||
source-list-map@^2.0.0:
|
source-list-map@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user