mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Convert HUI-ROOT to Lit Element (#2264)
* Convert HUI-ROOT to Lit Element * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Update src/panels/lovelace/hui-root.ts Co-Authored-By: balloob <paulus@home-assistant.io> * Apply suggestions from code review Co-Authored-By: balloob <paulus@home-assistant.io> * Address comments
This commit is contained in:
commit
ccc6262026
@ -3,6 +3,8 @@ import { HomeAssistant } from "../types";
|
||||
export interface LovelaceConfig {
|
||||
title?: string;
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
resources?: Array<{ type: "css" | "js" | "module" | "html"; url: string }>;
|
||||
}
|
||||
|
||||
export interface LovelaceViewConfig {
|
||||
@ -13,6 +15,8 @@ export interface LovelaceViewConfig {
|
||||
path?: string;
|
||||
icon?: string;
|
||||
theme?: string;
|
||||
panel?: boolean;
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface LovelaceCardConfig {
|
||||
|
@ -131,4 +131,10 @@ class LovelaceFullConfigEditor extends hassLocalizeLitMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-editor": LovelaceFullConfigEditor;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-editor", LovelaceFullConfigEditor);
|
||||
|
@ -1,454 +0,0 @@
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/app-route/app-route";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-menu-button/paper-menu-button";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import scrollToTarget from "../../common/dom/scroll-to-target";
|
||||
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
import localizeMixin from "../../mixins/localize-mixin";
|
||||
import NavigateMixin from "../../mixins/navigate-mixin";
|
||||
|
||||
import "../../layouts/ha-app-layout";
|
||||
import "../../components/ha-start-voice-button";
|
||||
import "../../components/ha-icon";
|
||||
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
|
||||
import { subscribeNotifications } from "../../data/ws-notifications";
|
||||
import { computeNotifications } from "./common/compute-notifications";
|
||||
import "./components/notifications/hui-notification-drawer";
|
||||
import "./components/notifications/hui-notifications-button";
|
||||
import "./hui-unused-entities";
|
||||
import "./hui-view";
|
||||
import debounce from "../../common/util/debounce";
|
||||
import createCardElement from "./common/create-card-element";
|
||||
import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
|
||||
|
||||
// CSS and JS should only be imported once. Modules and HTML are safe.
|
||||
const CSS_CACHE = {};
|
||||
const JS_CACHE = {};
|
||||
|
||||
class HUIRoot extends NavigateMixin(
|
||||
EventsMixin(localizeMixin(PolymerElement))
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include='ha-style'>
|
||||
:host {
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
ha-app-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
paper-tab.iron-selected .edit-view-icon{
|
||||
display: inline-flex;
|
||||
}
|
||||
.edit-view-icon {
|
||||
padding-left: 8px;
|
||||
display: none;
|
||||
}
|
||||
#add-view {
|
||||
position: absolute;
|
||||
height: 44px;
|
||||
}
|
||||
#add-view ha-icon {
|
||||
background-color: var(--accent-color);
|
||||
border-radius: 5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
app-toolbar a {
|
||||
color: var(--text-primary-color, white);
|
||||
}
|
||||
paper-button.warning:not([disabled]) {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
#view {
|
||||
min-height: calc(100vh - 112px);
|
||||
/**
|
||||
* Since we only set min-height, if child nodes need percentage
|
||||
* heights they must use absolute positioning so we need relative
|
||||
* positioning here.
|
||||
*
|
||||
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
#view.tabs-hidden {
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<app-route route="[[route]]" pattern="/:view" data="{{routeData}}"></app-route>
|
||||
<hui-notification-drawer
|
||||
hass="[[hass]]"
|
||||
notifications="[[_notifications]]"
|
||||
open="{{notificationsOpen}}"
|
||||
narrow="[[narrow]]"
|
||||
></hui-notification-drawer>
|
||||
<ha-app-layout id="layout">
|
||||
<app-header slot="header" effects="waterfall" fixed condenses>
|
||||
<template is='dom-if' if="[[!_editMode]]">
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||
<div main-title>[[_computeTitle(config)]]</div>
|
||||
<hui-notifications-button
|
||||
hass="[[hass]]"
|
||||
notifications-open="{{notificationsOpen}}"
|
||||
notifications="[[_notifications]]"
|
||||
></hui-notifications-button>
|
||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
>
|
||||
<paper-icon-button icon="hass:dots-vertical" slot="dropdown-trigger"></paper-icon-button>
|
||||
<paper-listbox on-iron-select="_deselect" slot="dropdown-content">
|
||||
<template is='dom-if' if="[[_yamlMode]]">
|
||||
<paper-item on-click="_handleRefresh">Refresh</paper-item>
|
||||
</template>
|
||||
<paper-item on-click="_handleUnusedEntities">Unused entities</paper-item>
|
||||
<paper-item on-click="_editModeEnable">[[localize("ui.panel.lovelace.editor.configure_ui")]]</paper-item>
|
||||
<template is='dom-if' if="[[_storageMode]]">
|
||||
<paper-item on-click="_handleFullEditor">Raw config editor</paper-item>
|
||||
</template>
|
||||
<paper-item on-click="_handleHelp">Help</paper-item>
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
</app-toolbar>
|
||||
</template>
|
||||
<template is='dom-if' if="[[_editMode]]">
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon='hass:close'
|
||||
on-click='_editModeDisable'
|
||||
></paper-icon-button>
|
||||
<div main-title>[[localize("ui.panel.lovelace.editor.header")]]</div>
|
||||
</app-toolbar>
|
||||
</template>
|
||||
|
||||
<div sticky hidden$="[[_computeTabsHidden(config.views, _editMode)]]">
|
||||
<paper-tabs scrollable selected="[[_curView]]" on-iron-activate="_handleViewSelected">
|
||||
<template is="dom-repeat" items="[[config.views]]">
|
||||
<paper-tab>
|
||||
<template is="dom-if" if="[[item.icon]]">
|
||||
<ha-icon title$="[[item.title]]" icon="[[item.icon]]"></ha-icon>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!item.icon]]">
|
||||
[[_computeTabTitle(item.title)]]
|
||||
</template>
|
||||
<template is='dom-if' if="[[_editMode]]">
|
||||
<ha-icon class="edit-view-icon" on-click="_editView" icon="hass:pencil"></ha-icon>
|
||||
</template>
|
||||
</paper-tab>
|
||||
</template>
|
||||
<template is='dom-if' if="[[_editMode]]">
|
||||
<paper-button id="add-view" on-click="_addView">
|
||||
<ha-icon title=[[localize("ui.panel.lovelace.editor.edit_view.add")]] icon="hass:plus"></ha-icon>
|
||||
</paper-button>
|
||||
</template>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
</app-header>
|
||||
<div id='view' on-rebuild-view='_debouncedConfigChanged'></div>
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
narrow: Boolean,
|
||||
showMenu: Boolean,
|
||||
hass: { type: Object, observer: "_hassChanged" },
|
||||
config: {
|
||||
type: Object,
|
||||
computed: "_computeConfig(lovelace)",
|
||||
observer: "_configChanged",
|
||||
},
|
||||
lovelace: { type: Object },
|
||||
columns: { type: Number, observer: "_columnsChanged" },
|
||||
_curView: { type: Number, value: 0 },
|
||||
route: { type: Object, observer: "_routeChanged" },
|
||||
notificationsOpen: { type: Boolean, value: false },
|
||||
_persistentNotifications: { type: Array, value: [] },
|
||||
_notifications: {
|
||||
type: Array,
|
||||
computed: "_updateNotifications(hass.states, _persistentNotifications)",
|
||||
},
|
||||
_yamlMode: {
|
||||
type: Boolean,
|
||||
computed: "_computeYamlMode(lovelace)",
|
||||
},
|
||||
_storageMode: {
|
||||
type: Boolean,
|
||||
computed: "_computeStorageMode(lovelace)",
|
||||
},
|
||||
_editMode: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
computed: "_computeEditMode(lovelace)",
|
||||
observer: "_editModeChanged",
|
||||
},
|
||||
routeData: Object,
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._debouncedConfigChanged = debounce(
|
||||
() => this._selectView(this._curView),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._unsubNotifications = subscribeNotifications(
|
||||
this.hass.connection,
|
||||
(notifications) => {
|
||||
this._persistentNotifications = notifications;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (typeof this._unsubNotifications === "function") {
|
||||
this._unsubNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
_updateNotifications(states, persistent) {
|
||||
if (!states) return persistent;
|
||||
|
||||
const configurator = computeNotifications(states);
|
||||
return persistent.concat(configurator);
|
||||
}
|
||||
|
||||
_routeChanged(route) {
|
||||
const views = this.config && this.config.views;
|
||||
if (route.path === "" && route.prefix === "/lovelace" && views) {
|
||||
this.navigate(`/lovelace/${views[0].path || 0}`, true);
|
||||
} else if (this.routeData.view) {
|
||||
const view = this.routeData.view;
|
||||
let index = 0;
|
||||
for (let i = 0; i < views.length; i++) {
|
||||
if (views[i].path === view || i === parseInt(view)) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index !== this._curView) this._selectView(index);
|
||||
}
|
||||
}
|
||||
|
||||
_computeViewPath(path, index) {
|
||||
return path || index;
|
||||
}
|
||||
|
||||
_computeTitle(config) {
|
||||
return config.title || "Home Assistant";
|
||||
}
|
||||
|
||||
_computeTabsHidden(views, editMode) {
|
||||
return views.length < 2 && !editMode;
|
||||
}
|
||||
|
||||
_computeTabTitle(title) {
|
||||
return title || "Unnamed view";
|
||||
}
|
||||
|
||||
_handleRefresh() {
|
||||
this.fire("config-refresh");
|
||||
}
|
||||
|
||||
_handleUnusedEntities() {
|
||||
this._selectView("unused");
|
||||
}
|
||||
|
||||
_deselect(ev) {
|
||||
ev.target.selected = null;
|
||||
}
|
||||
|
||||
_handleHelp() {
|
||||
window.open("https://www.home-assistant.io/lovelace/", "_blank");
|
||||
}
|
||||
|
||||
_handleFullEditor() {
|
||||
this.lovelace.enableFullEditMode();
|
||||
}
|
||||
|
||||
_editModeEnable() {
|
||||
if (this._yamlMode) {
|
||||
window.alert("The edit UI is not available when in YAML mode.");
|
||||
return;
|
||||
}
|
||||
this.lovelace.setEditMode(true);
|
||||
if (this.config.views.length < 2) {
|
||||
this.$.view.classList.remove("tabs-hidden");
|
||||
this.fire("iron-resize");
|
||||
}
|
||||
}
|
||||
|
||||
_editModeDisable() {
|
||||
this.lovelace.setEditMode(false);
|
||||
if (this.config.views.length < 2) {
|
||||
this.$.view.classList.add("tabs-hidden");
|
||||
this.fire("iron-resize");
|
||||
}
|
||||
}
|
||||
|
||||
_editModeChanged() {
|
||||
this._selectView(this._curView);
|
||||
}
|
||||
|
||||
_editView() {
|
||||
showEditViewDialog(this, {
|
||||
lovelace: this.lovelace,
|
||||
viewIndex: this._curView,
|
||||
});
|
||||
}
|
||||
|
||||
_addView() {
|
||||
showEditViewDialog(this, {
|
||||
lovelace: this.lovelace,
|
||||
});
|
||||
}
|
||||
|
||||
_handleViewSelected(ev) {
|
||||
const index = ev.detail.selected;
|
||||
this._navigateView(index);
|
||||
}
|
||||
|
||||
_navigateView(viewIndex) {
|
||||
if (viewIndex !== this._curView) {
|
||||
const path = this.config.views[viewIndex].path || viewIndex;
|
||||
this.navigate(`/lovelace/${path}`);
|
||||
}
|
||||
scrollToTarget(this, this.$.layout.header.scrollTarget);
|
||||
}
|
||||
|
||||
_selectView(viewIndex) {
|
||||
this._curView = viewIndex;
|
||||
|
||||
// Recreate a new element to clear the applied themes.
|
||||
const root = this.$.view;
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
let view;
|
||||
let background = this.config.background || "";
|
||||
|
||||
if (viewIndex === "unused") {
|
||||
view = document.createElement("hui-unused-entities");
|
||||
view.setConfig(this.config);
|
||||
} else {
|
||||
const viewConfig = this.config.views[this._curView];
|
||||
if (!viewConfig) {
|
||||
this._editModeEnable();
|
||||
return;
|
||||
}
|
||||
if (viewConfig.panel) {
|
||||
view = createCardElement(viewConfig.cards[0]);
|
||||
view.isPanel = true;
|
||||
} else {
|
||||
view = document.createElement("hui-view");
|
||||
view.lovelace = this.lovelace;
|
||||
view.config = viewConfig;
|
||||
view.columns = this.columns;
|
||||
view.index = viewIndex;
|
||||
}
|
||||
if (viewConfig.background) background = viewConfig.background;
|
||||
}
|
||||
|
||||
this.$.view.style.background = background;
|
||||
|
||||
view.hass = this.hass;
|
||||
root.appendChild(view);
|
||||
}
|
||||
|
||||
_hassChanged(hass) {
|
||||
if (!this.$.view.lastChild) return;
|
||||
this.$.view.lastChild.hass = hass;
|
||||
}
|
||||
|
||||
_configChanged(config) {
|
||||
this._loadResources(config.resources || []);
|
||||
// On config change, recreate the view from scratch.
|
||||
this._selectView(this._curView);
|
||||
this.$.view.classList.toggle("tabs-hidden", config.views.length < 2);
|
||||
}
|
||||
|
||||
_columnsChanged(columns) {
|
||||
if (!this.$.view.lastChild) return;
|
||||
this.$.view.lastChild.columns = columns;
|
||||
}
|
||||
|
||||
_loadResources(resources) {
|
||||
resources.forEach((resource) => {
|
||||
switch (resource.type) {
|
||||
case "css":
|
||||
if (resource.url in CSS_CACHE) break;
|
||||
CSS_CACHE[resource.url] = loadCSS(resource.url);
|
||||
break;
|
||||
|
||||
case "js":
|
||||
if (resource.url in JS_CACHE) break;
|
||||
JS_CACHE[resource.url] = loadJS(resource.url);
|
||||
break;
|
||||
|
||||
case "module":
|
||||
loadModule(resource.url);
|
||||
break;
|
||||
|
||||
case "html":
|
||||
import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href").then(
|
||||
({ importHref }) => importHref(resource.url)
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line
|
||||
console.warn("Unknown resource type specified: ${resource.type}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_computeConfig(lovelace) {
|
||||
return lovelace ? lovelace.config : null;
|
||||
}
|
||||
|
||||
_computeYamlMode(lovelace) {
|
||||
return lovelace ? lovelace.mode === "yaml" : false;
|
||||
}
|
||||
|
||||
_computeStorageMode(lovelace) {
|
||||
return lovelace ? lovelace.mode === "storage" : false;
|
||||
}
|
||||
|
||||
_computeEditMode(lovelace) {
|
||||
return lovelace ? lovelace.editMode : false;
|
||||
}
|
||||
}
|
||||
customElements.define("hui-root", HUIRoot);
|
584
src/panels/lovelace/hui-root.ts
Normal file
584
src/panels/lovelace/hui-root.ts
Normal file
@ -0,0 +1,584 @@
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import { TemplateResult } from "lit-html";
|
||||
import { classMap } from "lit-html/directives/classMap";
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/app-route/app-route";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-menu-button/paper-menu-button";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
|
||||
import scrollToTarget from "../../common/dom/scroll-to-target";
|
||||
|
||||
import "../../layouts/ha-app-layout";
|
||||
import "../../components/ha-start-voice-button";
|
||||
import "../../components/ha-icon";
|
||||
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
|
||||
import { subscribeNotifications } from "../../data/ws-notifications";
|
||||
import debounce from "../../common/util/debounce";
|
||||
import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { LovelaceConfig } from "../../data/lovelace";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeNotifications } from "./common/compute-notifications";
|
||||
import "./components/notifications/hui-notification-drawer";
|
||||
import "./components/notifications/hui-notifications-button";
|
||||
import "./hui-unused-entities";
|
||||
import "./hui-view";
|
||||
import createCardElement from "./common/create-card-element";
|
||||
import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
|
||||
import { Lovelace } from "./types";
|
||||
|
||||
// CSS and JS should only be imported once. Modules and HTML are safe.
|
||||
const CSS_CACHE = {};
|
||||
const JS_CACHE = {};
|
||||
|
||||
class HUIRoot extends hassLocalizeLitMixin(LitElement) {
|
||||
public narrow?: boolean;
|
||||
public showMenu?: boolean;
|
||||
public hass?: HomeAssistant;
|
||||
public lovelace?: Lovelace;
|
||||
public columns?: number;
|
||||
public route?: { path: string; prefix: string };
|
||||
private _routeData?: { view: string };
|
||||
private _curView: number | "unused";
|
||||
private notificationsOpen?: boolean;
|
||||
private _persistentNotifications?: Notification[];
|
||||
private _haStyle?: DocumentFragment;
|
||||
|
||||
private _debouncedConfigChanged: () => void;
|
||||
private _unsubNotifications?: () => void;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
narrow: {},
|
||||
showMenu: {},
|
||||
hass: {},
|
||||
lovelace: {},
|
||||
columns: {},
|
||||
route: {},
|
||||
_routeData: {},
|
||||
_curView: {},
|
||||
notificationsOpen: {},
|
||||
_persistentNotifications: {},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._curView = 0;
|
||||
this._debouncedConfigChanged = debounce(
|
||||
() => this._selectView(this._curView),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._unsubNotifications = subscribeNotifications(
|
||||
this.hass!.connection,
|
||||
(notifications) => {
|
||||
this._persistentNotifications = notifications;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubNotifications) {
|
||||
this._unsubNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<app-route .route="${this.route}" pattern="/:view" data="${
|
||||
this._routeData
|
||||
}" @data-changed="${this._routeDataChanged}"></app-route>
|
||||
<hui-notification-drawer
|
||||
.hass="${this.hass}"
|
||||
.notifications="${this._notifications}"
|
||||
.open="${this.notificationsOpen}"
|
||||
@open-changed="${this._handleNotificationsOpenChanged}"
|
||||
.narrow="${this.narrow}"
|
||||
></hui-notification-drawer>
|
||||
<ha-app-layout id="layout">
|
||||
<app-header slot="header" effects="waterfall" fixed condenses>
|
||||
${
|
||||
this._editMode
|
||||
? html`
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon="hass:close"
|
||||
@click="${this._editModeDisable}"
|
||||
></paper-icon-button>
|
||||
<div main-title>
|
||||
${this.localize("ui.panel.lovelace.editor.header")}
|
||||
</div>
|
||||
</app-toolbar>
|
||||
`
|
||||
: html`
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
.narrow="${this.narrow}"
|
||||
.showMenu="${this.showMenu}"
|
||||
></ha-menu-button>
|
||||
<div main-title>${this.config.title || "Home Assistant"}</div>
|
||||
<hui-notifications-button
|
||||
.hass="${this.hass}"
|
||||
.notificationsOpen="{{notificationsOpen}}"
|
||||
.notifications="${this._notifications}"
|
||||
></hui-notifications-button>
|
||||
<ha-start-voice-button
|
||||
.hass="${this.hass}"
|
||||
></ha-start-voice-button>
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
>
|
||||
<paper-icon-button
|
||||
icon="hass:dots-vertical"
|
||||
slot="dropdown-trigger"
|
||||
></paper-icon-button>
|
||||
<paper-listbox
|
||||
@iron-select="${this._deselect}"
|
||||
slot="dropdown-content"
|
||||
>
|
||||
${
|
||||
this._yamlMode
|
||||
? html`
|
||||
<paper-item @click="${this._handleRefresh}"
|
||||
>Refresh</paper-item
|
||||
>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<paper-item @click="${this._handleUnusedEntities}"
|
||||
>Unused entities</paper-item
|
||||
>
|
||||
<paper-item @click="${this._editModeEnable}"
|
||||
>${
|
||||
this.localize("ui.panel.lovelace.editor.configure_ui")
|
||||
}</paper-item
|
||||
>
|
||||
${
|
||||
this._storageMode
|
||||
? html`
|
||||
<paper-item
|
||||
@click="${this.lovelace!.enableFullEditMode}"
|
||||
>Raw config editor</paper-item
|
||||
>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<paper-item @click="${this._handleHelp}">Help</paper-item>
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
</app-toolbar>
|
||||
`
|
||||
}
|
||||
|
||||
${
|
||||
this.lovelace!.config.views.length > 1 || this._editMode
|
||||
? html`
|
||||
<div sticky>
|
||||
<paper-tabs
|
||||
scrollable
|
||||
.selected="${this._curView}"
|
||||
@iron-activate="${this._handleViewSelected}"
|
||||
>
|
||||
${
|
||||
this.lovelace!.config.views.map(
|
||||
(view) => html`
|
||||
<paper-tab>
|
||||
${
|
||||
view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
title="${view.title}"
|
||||
.icon="${view.icon}"
|
||||
></ha-icon>
|
||||
`
|
||||
: view.title || "Unnamed view"
|
||||
}
|
||||
${
|
||||
this._editMode
|
||||
? html`
|
||||
<ha-icon
|
||||
class="edit-view-icon"
|
||||
@click="${this._editView}"
|
||||
icon="hass:pencil"
|
||||
></ha-icon>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</paper-tab>
|
||||
`
|
||||
)
|
||||
}
|
||||
${
|
||||
this._editMode
|
||||
? html`
|
||||
<paper-button
|
||||
id="add-view"
|
||||
@click="${this._addView}"
|
||||
>
|
||||
<ha-icon
|
||||
title="${
|
||||
this.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.add"
|
||||
)
|
||||
}"
|
||||
icon="hass:plus"
|
||||
></ha-icon>
|
||||
</paper-button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</paper-tabs>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</app-header>
|
||||
<div id='view' class="${classMap({
|
||||
"tabs-hidden": this.lovelace!.config.views.length < 2,
|
||||
})}" @rebuild-view='${this._debouncedConfigChanged}'></div>
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderStyle(): TemplateResult {
|
||||
if (!this._haStyle) {
|
||||
this._haStyle = document.importNode(
|
||||
(document.getElementById("ha-style")!
|
||||
.children[0] as HTMLTemplateElement).content,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._haStyle}
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
ha-app-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
paper-tab.iron-selected .edit-view-icon {
|
||||
display: inline-flex;
|
||||
}
|
||||
.edit-view-icon {
|
||||
padding-left: 8px;
|
||||
display: none;
|
||||
}
|
||||
#add-view {
|
||||
position: absolute;
|
||||
height: 44px;
|
||||
}
|
||||
#add-view ha-icon {
|
||||
background-color: var(--accent-color);
|
||||
border-radius: 5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
app-toolbar a {
|
||||
color: var(--text-primary-color, white);
|
||||
}
|
||||
paper-button.warning:not([disabled]) {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
#view {
|
||||
min-height: calc(100vh - 112px);
|
||||
/**
|
||||
* Since we only set min-height, if child nodes need percentage
|
||||
* heights they must use absolute positioning so we need relative
|
||||
* positioning here.
|
||||
*
|
||||
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
#view.tabs-hidden {
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const view = this._view;
|
||||
const huiView = view.lastChild as any;
|
||||
|
||||
if (changedProperties.has("columns") && huiView) {
|
||||
(this._view.lastChild as any).columns = this.columns;
|
||||
}
|
||||
|
||||
if (changedProperties.has("hass") && huiView) {
|
||||
huiView.hass = this.hass;
|
||||
}
|
||||
|
||||
if (changedProperties.has("route")) {
|
||||
const views = this.config && this.config.views;
|
||||
if (
|
||||
this.route!.path === "" &&
|
||||
this.route!.prefix === "/lovelace" &&
|
||||
views
|
||||
) {
|
||||
navigate(this, `/lovelace/${views[0].path || 0}`, true);
|
||||
} else if (this._routeData!.view) {
|
||||
const selectedView = this._routeData!.view;
|
||||
const selectedViewInt = parseInt(selectedView, 10);
|
||||
let index = 0;
|
||||
for (let i = 0; i < views.length; i++) {
|
||||
if (views[i].path === selectedView || i === selectedViewInt) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index !== this._curView) {
|
||||
this._selectView(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("lovelace")) {
|
||||
const oldLovelace = changedProperties.get("lovelace") as
|
||||
| Lovelace
|
||||
| undefined;
|
||||
|
||||
if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) {
|
||||
this._loadResources(this.lovelace!.config.resources || []);
|
||||
// On config change, recreate the view from scratch.
|
||||
this._selectView(this._curView);
|
||||
}
|
||||
|
||||
if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) {
|
||||
this._editModeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private get _notifications() {
|
||||
return this._updateNotifications(
|
||||
this.hass!.states,
|
||||
this._persistentNotifications! || []
|
||||
);
|
||||
}
|
||||
|
||||
private get config(): LovelaceConfig {
|
||||
return this.lovelace!.config;
|
||||
}
|
||||
|
||||
private get _yamlMode(): boolean {
|
||||
return this.lovelace!.mode === "yaml";
|
||||
}
|
||||
|
||||
private get _storageMode(): boolean {
|
||||
return this.lovelace!.mode === "storage";
|
||||
}
|
||||
|
||||
private get _editMode() {
|
||||
return this.lovelace!.editMode;
|
||||
}
|
||||
|
||||
private get _layout(): any {
|
||||
return this.shadowRoot!.getElementById("layout");
|
||||
}
|
||||
|
||||
private get _view(): HTMLDivElement {
|
||||
return this.shadowRoot!.getElementById("view") as HTMLDivElement;
|
||||
}
|
||||
|
||||
private _routeDataChanged(ev): void {
|
||||
this._routeData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleNotificationsOpenChanged(ev): void {
|
||||
this.notificationsOpen = ev.detail.value;
|
||||
}
|
||||
|
||||
private _updateNotifications(
|
||||
states: HassEntities,
|
||||
persistent: Array<unknown>
|
||||
): Array<unknown> {
|
||||
const configurator = computeNotifications(states);
|
||||
return persistent.concat(configurator);
|
||||
}
|
||||
|
||||
private _handleRefresh(): void {
|
||||
fireEvent(this, "config-refresh");
|
||||
}
|
||||
|
||||
private _handleUnusedEntities(): void {
|
||||
this._selectView("unused");
|
||||
}
|
||||
|
||||
private _deselect(ev): void {
|
||||
ev.target.selected = null;
|
||||
}
|
||||
|
||||
private _handleHelp(): void {
|
||||
window.open("https://www.home-assistant.io/lovelace/", "_blank");
|
||||
}
|
||||
|
||||
private _editModeEnable(): void {
|
||||
if (this._yamlMode) {
|
||||
window.alert("The edit UI is not available when in YAML mode.");
|
||||
return;
|
||||
}
|
||||
this.lovelace!.setEditMode(true);
|
||||
if (this.config.views.length < 2) {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
}
|
||||
|
||||
private _editModeDisable(): void {
|
||||
this.lovelace!.setEditMode(false);
|
||||
if (this.config.views.length < 2) {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
}
|
||||
|
||||
private _editModeChanged(): void {
|
||||
this._selectView(this._curView);
|
||||
}
|
||||
|
||||
private _editView() {
|
||||
showEditViewDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
viewIndex: this._curView as number,
|
||||
});
|
||||
}
|
||||
|
||||
private _addView() {
|
||||
showEditViewDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleViewSelected(ev) {
|
||||
const index = ev.detail.selected;
|
||||
this._navigateView(index);
|
||||
}
|
||||
|
||||
private _navigateView(viewIndex: number): void {
|
||||
if (viewIndex !== this._curView) {
|
||||
const path = this.config.views[viewIndex].path || viewIndex;
|
||||
navigate(this, `/lovelace/${path}`);
|
||||
}
|
||||
scrollToTarget(this, this._layout.header.scrollTarget);
|
||||
}
|
||||
|
||||
private _selectView(viewIndex: HUIRoot["_curView"]): void {
|
||||
this._curView = viewIndex;
|
||||
|
||||
// Recreate a new element to clear the applied themes.
|
||||
const root = this._view;
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
let view;
|
||||
let background = this.config.background || "";
|
||||
|
||||
if (viewIndex === "unused") {
|
||||
view = document.createElement("hui-unused-entities");
|
||||
view.setConfig(this.config);
|
||||
} else {
|
||||
const viewConfig = this.config.views[this._curView];
|
||||
if (!viewConfig) {
|
||||
this._editModeEnable();
|
||||
return;
|
||||
}
|
||||
if (viewConfig.panel && viewConfig.cards && viewConfig.cards.length > 0) {
|
||||
view = createCardElement(viewConfig.cards[0]);
|
||||
view.isPanel = true;
|
||||
} else {
|
||||
view = document.createElement("hui-view");
|
||||
view.lovelace = this.lovelace;
|
||||
view.config = viewConfig;
|
||||
view.columns = this.columns;
|
||||
view.index = viewIndex;
|
||||
}
|
||||
if (viewConfig.background) {
|
||||
background = viewConfig.background;
|
||||
}
|
||||
}
|
||||
|
||||
this._view.style.background = background;
|
||||
|
||||
view.hass = this.hass;
|
||||
root.appendChild(view);
|
||||
}
|
||||
|
||||
private _loadResources(resources) {
|
||||
resources.forEach((resource) => {
|
||||
switch (resource.type) {
|
||||
case "css":
|
||||
if (resource.url in CSS_CACHE) {
|
||||
break;
|
||||
}
|
||||
CSS_CACHE[resource.url] = loadCSS(resource.url);
|
||||
break;
|
||||
|
||||
case "js":
|
||||
if (resource.url in JS_CACHE) {
|
||||
break;
|
||||
}
|
||||
JS_CACHE[resource.url] = loadJS(resource.url);
|
||||
break;
|
||||
|
||||
case "module":
|
||||
loadModule(resource.url);
|
||||
break;
|
||||
|
||||
case "html":
|
||||
import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href").then(
|
||||
({ importHref }) => importHref(resource.url)
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// tslint:disable-next-line
|
||||
console.warn(`Unknown resource type specified: ${resource.type}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-root": HUIRoot;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hui-root", HUIRoot);
|
Loading…
x
Reference in New Issue
Block a user