Popup-card

This commit is contained in:
Thomas Lovén 2022-07-19 22:58:27 +00:00
parent a4085ed3ab
commit 30d8f027dc
8 changed files with 834 additions and 15 deletions

File diff suppressed because one or more lines are too long

View File

@ -61,6 +61,15 @@ popup:
description: "Popup content (Test or lovelace card configuration)" description: "Popup content (Test or lovelace card configuration)"
selector: selector:
object: object:
size:
name: Size
selector:
select:
mode: dropdown
options:
- normal
- wide
- fullscreen
right_button: right_button:
name: Right button name: Right button
description: Text of the right button description: Text of the right button
@ -99,7 +108,7 @@ popup:
number: number:
mode: box mode: box
timeout_action: timeout_action:
name: Timeout name: Timeout action
description: Action to perform when popup is closed by timeout description: Action to perform when popup is closed by timeout
selector: selector:
object: object:

View File

@ -75,3 +75,13 @@ export const loadLoadCardHelpers = async () => {
]); ]);
await routes?.routes?.a?.load?.(); await routes?.routes?.a?.load?.();
}; };
export const loadHaForm = async () => {
if (customElements.get("ha-form")) return;
await loadLoadCardHelpers();
const helpers = await window.loadCardHelpers();
if (!helpers) return;
const card = await helpers.createCardElement({ type: "entity" });
if (!card) return;
await card.getConfigElement();
};

View File

@ -13,6 +13,7 @@ import { ActivityMixin } from "./activity";
import "./popups"; import "./popups";
import { PopupMixin } from "./popups"; import { PopupMixin } from "./popups";
import pjson from "../../package.json"; import pjson from "../../package.json";
import "./popup-card";
/* /*
TODO: TODO:
@ -24,6 +25,7 @@ import pjson from "../../package.json";
- Card-mod integration - Card-mod integration
X Timeout X Timeout
X Fullscreen X Fullscreen
x Popup-card
x Motion/occupancy tracker x Motion/occupancy tracker
x Information about interaction requirement x Information about interaction requirement
x Information about fullykiosk x Information about fullykiosk

View File

@ -0,0 +1,264 @@
import { LitElement, html, css } from "lit";
import { property, query, state } from "lit/decorators.js";
import { loadHaForm } from "../helpers";
const configSchema = [
{
name: "entity",
label: "Entity",
selector: { entity: {} },
},
{
name: "title",
label: "Title",
selector: { text: {} },
},
{
name: "size",
selector: {
select: { mode: "dropdown", options: ["normal", "wide", "fullscreen"] },
},
},
{
type: "grid",
schema: [
{
name: "right_button",
label: "Right button",
selector: { text: {} },
},
{
name: "left_button",
label: "Left button",
selector: { text: {} },
},
],
},
{
type: "grid",
schema: [
{
name: "right_button_action",
label: "Right button action",
selector: { object: {} },
},
{
name: "left_button_action",
label: "Left button action",
selector: { object: {} },
},
],
},
{
type: "grid",
schema: [
{
name: "dismissable",
label: "User dismissable",
selector: { boolean: {} },
},
{
name: "timeout",
label: "Auto close timeout (ms)",
selector: { number: { mode: "box" } },
},
],
},
{
type: "grid",
schema: [
{
name: "dismiss_action",
label: "Dismiss action",
selector: { object: {} },
},
{
name: "timeout_action",
label: "Timeout action",
selector: { object: {} },
},
],
},
];
class PopupCardEditor extends LitElement {
@state() _config;
@property() lovelace;
@property() hass;
@state() _selectedTab = 0;
@state() _cardGUIMode = true;
@state() _cardGUIModeAvailable = true;
@query("hui-card-element-editor") private _cardEditorEl?;
setConfig(config) {
this._config = config;
}
connectedCallback() {
super.connectedCallback();
loadHaForm();
}
_handleSwitchTab(ev: CustomEvent) {
this._selectedTab = parseInt(ev.detail.index, 10);
}
_configChanged(ev: CustomEvent) {
ev.stopPropagation();
if (!this._config) return;
this._config = { ...ev.detail.value };
this.dispatchEvent(
new CustomEvent("config-changed", { detail: { config: this._config } })
);
}
_cardConfigChanged(ev: CustomEvent) {
ev.stopPropagation();
if (!this._config) return;
const card = { ...ev.detail.config };
this._config = { ...this._config, card };
this._cardGUIModeAvailable = ev.detail.guiModeAvailable;
this.dispatchEvent(
new CustomEvent("config-changed", { detail: { config: this._config } })
);
}
_toggleCardMode(ev) {
this._cardEditorEl?.toggleMode();
}
_deleteCard(ev) {
if (!this._config) return;
this._config = { ...this._config };
delete this._config.card;
this.dispatchEvent(
new CustomEvent("config-changed", { detail: { config: this._config } })
);
}
_cardGUIModeChanged(ev: CustomEvent) {
ev.stopPropagation();
this._cardGUIMode = ev.detail.guiMode;
this._cardGUIModeAvailable = ev.detail.guiModeAvailable;
}
render() {
if (!this.hass || !this._config) {
return html``;
}
return html`
<div class="card-config">
<div class="toolbar">
<mwc-tab-bar
.activeIndex=${this._selectedTab}
@MDCTabBar:activated=${this._handleSwitchTab}
>
<mwc-tab .label=${"Settings"}></mwc-tab>
<mwc-tab .label=${"Card"}></mwc-tab>
</mwc-tab-bar>
</div>
<div id="editor">
${[this._renderSettingsEditor, this._renderCardEditor][
this._selectedTab
].bind(this)()}
</div>
</div>
`;
}
_renderSettingsEditor() {
return html`<div class="box">
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${configSchema}
.computeLabel=${(s) => s.label ?? s.name}
@value-changed=${this._configChanged}
></ha-form>
</div>`;
}
_renderCardEditor() {
return html`
<div class="box cards">
${this._config.card
? html`
<div class="toolbar">
<mwc-button
@click=${this._toggleCardMode}
.disabled=${!this._cardGUIModeAvailable}
class="gui-mode-button"
>
${!this._cardEditorEl || this._cardGUIMode
? "Show code editor"
: "Show visual editor"}
</mwc-button>
<mwc-button
.title=${"Change card type"}
@click=${this._deleteCard}
>
Change card type
</mwc-button>
</div>
<hui-card-element-editor
.hass=${this.hass}
.lovelace=${this.lovelace}
.value=${this._config.card}
@config-changed=${this._cardConfigChanged}
@GUImode-changed=${this._cardGUIModeChanged}
></hui-card-element-editor>
`
: html`
<hui-card-picker
.hass=${this.hass}
.lovelace=${this.lovelace}
@config-changed=${this._cardConfigChanged}
></hui-card-picker>
`}
</div>
`;
}
static get styles() {
return css`
mwc-tab-bar {
border-bottom: 1px solid var(--divider-color);
}
.box {
margin-top: 8px;
border: 1px solid var(--divider-color);
padding: 12px;
}
.box .toolbar {
display: flex;
justify-content: flex-end;
width: 100%;
gap: 8px;
}
.gui-mode-button {
margin-right: auto;
}
`;
}
}
(async () => {
while (!window.browser_mod) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
await window.browser_mod.connectionPromise;
if (!customElements.get("popup-card-editor"))
customElements.define("popup-card-editor", PopupCardEditor);
(window as any).customCards = (window as any).customCards || [];
(window as any).customCards.push({
type: "popup-card",
name: "Popup card",
preview: false,
description:
"Replace the more-info dialog for a given entity in the view that includes this card. (Browser Mod)",
});
})();

123
js/plugin/popup-card.ts Normal file
View File

@ -0,0 +1,123 @@
import { LitElement, html, css } from "lit";
import { property, state } from "lit/decorators.js";
import "./popup-card-editor";
class PopupCard extends LitElement {
@property() hass;
@state() _config;
@property({ attribute: "edit-mode", reflect: true }) editMode;
@state() _element;
static getConfigElement() {
return document.createElement("popup-card-editor");
}
static getStubConfig(hass, entities) {
const entity = entities[0];
return {
entity,
title: "Custom popup",
dismissable: true,
card: { type: "markdown", content: "This replaces the more-info dialog" },
};
}
constructor() {
super();
this.popup = this.popup.bind(this);
}
setConfig(config) {
this._config = config;
(async () => {
const ch = await window.loadCardHelpers();
this._element = await ch.createCardElement(config.card);
this._element.hass = this.hass;
})();
}
async connectedCallback() {
super.connectedCallback();
window.addEventListener("hass-more-info", this.popup);
if (this.parentElement.localName === "hui-card-preview") {
this.editMode = true;
}
}
async disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("hass-more-info", this.popup);
}
popup(ev: CustomEvent) {
if (ev.detail?.entityId === this._config.entity) {
ev.stopPropagation();
ev.preventDefault();
const config = { ...this._config };
delete config.card;
window.browser_mod?.service("popup", {
content: this._config.card,
...this._config,
});
setTimeout(
() =>
this.dispatchEvent(
new CustomEvent("hass-more-info", {
bubbles: true,
composed: true,
cancelable: false,
detail: { entityID: "." },
})
),
50
);
}
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has("hass")) {
if (this._element) this._element.hass = this.hass;
}
}
render() {
if (!this.editMode) return html``;
return html` <ha-card>
<div>
<h2>${this._config.title}</h2>
</div>
${this._element}</ha-card
>`;
}
static get styles() {
return css`
:host {
display: none !important;
}
:host([edit-mode="true"]) {
display: block !important;
border: 1px solid var(--primary-color);
}
h2 {
padding-left: 16px;
padding-top: 16px;
margin: 0;
}
`;
}
}
(async () => {
while (!window.browser_mod) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
await window.browser_mod.connectionPromise;
if (!customElements.get("popup-card"))
customElements.define("popup-card", PopupCard);
})();

View File

@ -12,6 +12,8 @@ class BrowserModPopup extends LitElement {
@property() right_button; @property() right_button;
@property() left_button; @property() left_button;
@property() dismissable; @property() dismissable;
@property({ reflect: true }) wide;
@property({ reflect: true }) fullscreen;
_actions; _actions;
timeout; timeout;
_timeoutStart; _timeoutStart;
@ -47,6 +49,7 @@ class BrowserModPopup extends LitElement {
dismiss_action = undefined, dismiss_action = undefined,
timeout = undefined, timeout = undefined,
timeout_action = undefined, timeout_action = undefined,
size = undefined,
} = {} } = {}
) { ) {
this.title = title; this.title = title;
@ -76,6 +79,8 @@ class BrowserModPopup extends LitElement {
dismiss_action, dismiss_action,
timeout_action, timeout_action,
}; };
this.wide = size === "wide" ? "" : undefined;
this.fullscreen = size === "fullscreen" ? "" : undefined;
} }
_primary() { _primary() {

View File

@ -68,10 +68,7 @@ export const ServicesMixin = (SuperClass) => {
]; ];
for (const service of cmds) { for (const service of cmds) {
this.addEventListener(`command-${service}`, (ev) => { this.addEventListener(`command-${service}`, (ev) => {
this._service_action({ this.service(service, ev.detail);
service,
data: ev.detail,
});
}); });
} }
@ -82,6 +79,10 @@ export const ServicesMixin = (SuperClass) => {
}); });
} }
async service(service, data) {
this._service_action({ service, data });
}
async _service_action({ service, data }) { async _service_action({ service, data }) {
let _service: String = service; let _service: String = service;
if (!_service.startsWith("browser_mod.") && _service.includes(".")) { if (!_service.startsWith("browser_mod.") && _service.includes(".")) {