frontend/src/layouts/partial-panel-resolver.ts

239 lines
6.9 KiB
TypeScript

import { PolymerElement } from "@polymer/polymer";
import {
STATE_NOT_RUNNING,
STATE_RUNNING,
STATE_STARTING,
} from "home-assistant-js-websocket";
import { PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { deepActiveElement } from "../common/dom/deep-active-element";
import { deepEqual } from "../common/util/deep-equal";
import { getDefaultPanel } from "../data/panel";
import { CustomPanelInfo } from "../data/panel_custom";
import { HomeAssistant, Panels } from "../types";
import { removeLaunchScreen } from "../util/launch-screen";
import {
HassRouterPage,
RouteOptions,
RouterOptions,
} from "./hass-router-page";
const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
const COMPONENTS = {
energy: () => import("../panels/energy/ha-panel-energy"),
calendar: () => import("../panels/calendar/ha-panel-calendar"),
config: () => import("../panels/config/ha-panel-config"),
custom: () => import("../panels/custom/ha-panel-custom"),
"developer-tools": () =>
import("../panels/developer-tools/ha-panel-developer-tools"),
lovelace: () => import("../panels/lovelace/ha-panel-lovelace"),
history: () => import("../panels/history/ha-panel-history"),
iframe: () => import("../panels/iframe/ha-panel-iframe"),
logbook: () => import("../panels/logbook/ha-panel-logbook"),
mailbox: () => import("../panels/mailbox/ha-panel-mailbox"),
map: () => import("../panels/map/ha-panel-map"),
my: () => import("../panels/my/ha-panel-my"),
profile: () => import("../panels/profile/ha-panel-profile"),
"shopping-list": () =>
import("../panels/shopping-list/ha-panel-shopping-list"),
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
};
@customElement("partial-panel-resolver")
class PartialPanelResolver extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
private _waitForStart = false;
private _disconnectedPanel?: HTMLElement;
private _disconnectedActiveElement?: HTMLElement;
private _hiddenTimeout?: number;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
// Attach listeners for visibility
document.addEventListener(
"visibilitychange",
() => this._checkVisibility(),
false
);
document.addEventListener("resume", () => this._checkVisibility());
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (
this._waitForStart &&
(this.hass.config.state === STATE_STARTING ||
this.hass.config.state === STATE_RUNNING)
) {
this._waitForStart = false;
this.rebuild();
}
if (this.hass.panels && (!oldHass || oldHass.panels !== this.hass.panels)) {
this._updateRoutes(oldHass?.panels);
}
}
protected createLoadingScreen() {
const el = super.createLoadingScreen();
el.rootnav = true;
el.hass = this.hass;
el.narrow = this.narrow;
return el;
}
protected updatePageEl(el) {
const hass = this.hass;
if ("setProperties" in el) {
// As long as we have Polymer panels
(el as PolymerElement).setProperties({
hass: this.hass,
narrow: this.narrow,
route: this.routeTail,
panel: hass.panels[this._currentPage],
});
} else {
el.hass = hass;
el.narrow = this.narrow;
el.route = this.routeTail;
el.panel = hass.panels[this._currentPage];
}
}
private _checkVisibility() {
if (this.hass.suspendWhenHidden === false) {
return;
}
if (document.hidden) {
this._onHidden();
} else {
this._onVisible();
}
}
private getRoutes(panels: Panels): RouterOptions {
const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => {
const data: RouteOptions = {
tag: `ha-panel-${panel.component_name}`,
cache: CACHE_URL_PATHS.includes(panel.url_path),
};
if (panel.component_name in COMPONENTS) {
data.load = COMPONENTS[panel.component_name];
}
routes[panel.url_path] = data;
});
return {
beforeRender: (page) => {
if (!page || !routes[page]) {
return getDefaultPanel(this.hass).url_path;
}
return undefined;
},
showLoading: true,
routes,
};
}
private _onHidden() {
this._hiddenTimeout = window.setTimeout(() => {
this._hiddenTimeout = undefined;
// setTimeout can be delayed in the background and only fire
// when we switch to the tab or app again (Hey Android!)
if (!document.hidden) {
return;
}
const curPanel = this.hass.panels[this._currentPage];
if (
this.lastChild &&
// iFrames will lose their state when disconnected
// Do not disconnect any iframe panel
curPanel.component_name !== "iframe" &&
// Do not disconnect any custom panel that embeds into iframe (ie hassio)
(curPanel.component_name !== "custom" ||
!(curPanel as CustomPanelInfo).config._panel_custom.embed_iframe)
) {
this._disconnectedPanel = this.lastChild as HTMLElement;
const activeEl = deepActiveElement(
this._disconnectedPanel.shadowRoot || undefined
);
if (activeEl instanceof HTMLElement) {
this._disconnectedActiveElement = activeEl;
}
this.removeChild(this.lastChild);
}
}, 300000);
window.addEventListener("focus", () => this._onVisible(), { once: true });
}
private _onVisible() {
if (this._hiddenTimeout) {
clearTimeout(this._hiddenTimeout);
this._hiddenTimeout = undefined;
}
if (this._disconnectedPanel) {
this.appendChild(this._disconnectedPanel);
this._disconnectedPanel = undefined;
}
if (this._disconnectedActiveElement) {
this._disconnectedActiveElement.focus();
this._disconnectedActiveElement = undefined;
}
}
private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
this.routerOptions = this.getRoutes(this.hass.panels);
if (
!this._waitForStart &&
this._currentPage &&
!this.hass.panels[this._currentPage]
) {
if (this.hass.config.state === STATE_NOT_RUNNING) {
this._waitForStart = true;
if (this.lastChild) {
this.removeChild(this.lastChild);
}
this.appendChild(this.createLoadingScreen());
return;
}
}
if (
!oldPanels ||
!deepEqual(
oldPanels[this._currentPage],
this.hass.panels[this._currentPage]
)
) {
await this.rebuild();
await this.pageRendered;
removeLaunchScreen();
}
}
}
declare global {
interface HTMLElementTagNameMap {
"partial-panel-resolver": PartialPanelResolver;
}
}