Update popup design. Add form popups

This commit is contained in:
Thomas Lovén 2022-08-03 21:21:25 +00:00
parent a641617671
commit bb7b7fa6ff
12 changed files with 289 additions and 131 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
"dependencies": ["panel_custom", "websocket_api", "http", "frontend", "lovelace"], "dependencies": ["panel_custom", "websocket_api", "http", "frontend", "lovelace"],
"codeowners": [], "codeowners": [],
"requirements": [], "requirements": [],
"version": "2.0.0b2", "version": "2.0.0b3",
"iot_class": "local_push", "iot_class": "local_push",
"config_flow": true "config_flow": true
} }

View File

@ -10,16 +10,38 @@ data:
left_button: Left button left_button: Left button
``` ```
![Popup](https://user-images.githubusercontent.com/1299821/180668969-c647f301-3f3d-4f3b-a1f8-d95af8b48873.png) ![Dialog Anatomy](https://user-images.githubusercontent.com/1299821/182708739-f89e3b2b-199f-43e0-bf04-e1dfc7075b2a.png)
## Displaying a dashboard card in a popup ## Size
The `size` parameter can be set to `normal`, `wide` and `fullscreen` with results as below (background blur has been exagerated for clarity):
![Normal Size](https://user-images.githubusercontent.com/1299821/182709146-439814f1-d479-4fc7-aab1-e28f5c9a13c7.png)
![Wide Size](https://user-images.githubusercontent.com/1299821/182709172-c98a9c23-5e58-4564-bcb7-1d187842948f.png)
![Fullscreen Size](https://user-images.githubusercontent.com/1299821/182709224-fb2e7b92-8a23-4422-95a0-f0f2835909e0.png)
## HTML content
```yaml ```yaml
service: browser_mod.popup service: browser_mod.popup
data: data:
title: The title title: HTML content
right_button: Right button content: |
left_button: Left button An <b>HTML</b> string.
<p> Pretty much any HTML works: <ha-icon icon="mdi:lamp" style="color: red;"></ha-icon>
```
![HTML content](https://user-images.githubusercontent.com/1299821/182710044-6fea3ba3-5262-4361-a131-691770340518.png)
## Dashboard card content
```yaml
service: browser_mod.popup
data:
title: HTML content
content: content:
type: entities type: entities
entities: entities:
@ -28,10 +50,53 @@ data:
- light.kitchen_lights - light.kitchen_lights
``` ```
![Popup with card](https://user-images.githubusercontent.com/1299821/180669077-bbc86831-3a8a-4e54-b098-d900d62d3508.png) ![Card content](https://user-images.githubusercontent.com/1299821/182710445-f09b74b8-dd53-4d65-8eba-0945fc1d418e.png)
## Form content
`content` can be a list of ha-form schemas and the popup will then contain a form for user input:
```
<ha-form schema>:
name: <string>
[label: <string>]
[default: <any>]
selector: <Home Assistant Selector>
```
| | |
|-|-|
| `name` | A unique parameter name |
| `label` | A description of the parameter |
| `default` | The default value for the parameter |
| `selector` | A [Home Assistant selector](https://www.home-assistant.io/docs/blueprint/selectors) |
The data from the form will be forwarded as data for any `right_button_action` or `left_button_action` of the popup.
```yaml
service: browser_mod.popup
data:
title: Form content
content:
- name: parameter_name
label: Descriptive name
selector:
text: null
- name: another_parameter
label: A number
default: 5
selector:
number:
min: 0
max: 10
slider: true
```
![Form content](https://user-images.githubusercontent.com/1299821/182712670-f3b4fdb7-84a9-49d1-a26f-2cdaa450fa0e.png)
## Actionable popups ## Actionable popups
Example of a popup with actions opening more popups or calling Home Assistant services:
```yaml ```yaml
service: browser_mod.popup service: browser_mod.popup
data: data:
@ -62,4 +127,35 @@ data:
entity_id: light.bed_light entity_id: light.bed_light
``` ```
![Advanced popup](https://user-images.githubusercontent.com/1299821/180670190-18cf8eee-cf18-47b9-84d1-e62ef327c615.gif) ![Multi-level popup](https://user-images.githubusercontent.com/1299821/182713421-708d0026-bcfa-4ba6-bbcd-3b85b584162d.gif)
## Forward form data
The following popup would ask the user for a list of rooms to vacuum and then populate the `params` parameter of the `vacuum.send_command` service call from the result:
```yaml
service: browser_mod.popup
data:
title: Where to vacuum?
right_button: Go!
right_button_action:
service: vacuum.send_command
data:
entity_id: vacuum.xiaomi
command: app_segment_clean
content:
- name: params
label: Rooms to clean
selector:
select:
multiple: true
options:
- label: Kitchen
value: 11
- label: Living room
value: 13
- label: Bedroom
value: 12
```
![Vacuum popup](https://user-images.githubusercontent.com/1299821/182713714-ef4149b1-217a-4d41-9737-714f5320c25c.png)

View File

@ -125,7 +125,7 @@ Display a popup dialog
service: browser_mod.popup service: browser_mod.popup
data: data:
[title: <string>] [title: <string>]
content: <string / Dashboard card configuration> content: <string / Dashboard card configuration / ha-form schema>
[size: <NORMAL/wide/fullscreen>] [size: <NORMAL/wide/fullscreen>]
[right_button: <string>] [right_button: <string>]
[right_button_action: <service call>] [right_button_action: <service call>]
@ -143,7 +143,7 @@ data:
| | | | | |
|---|---| |---|---|
|`title` | The title of the popup window.| |`title` | The title of the popup window.|
|`content`| HTML or a dashboard card configuration to display.| |`content`| HTML, a dashboard card configuration or ha-form schema to display.|
| `size` | `wide` will make the popup window wider. `fullscreen` will make it cover the entire screen. | | `size` | `wide` will make the popup window wider. `fullscreen` will make it cover the entire screen. |
| `right_button`| The text of the right action button.| | `right_button`| The text of the right action button.|
| `right_button_action`| Action to perform when the right action button is pressed. | | `right_button_action`| Action to perform when the right action button is pressed. |
@ -171,7 +171,9 @@ style:
Note that any Browser Mod services performed as `_action`s here will be performed only on the same Browser as initiated the action unless `browser_id` is given. Note that any Browser Mod services performed as `_action`s here will be performed only on the same Browser as initiated the action unless `browser_id` is given.
For usage examples, see [popups.md](popups.md). If a ha-form schema is used for `content` the resulting data will be inserted into the `data` for any `_action`.
See [popups.md](popups.md) for more information and usage examples.
## `browser_mod.close_popup` ## `browser_mod.close_popup`

View File

@ -14,9 +14,7 @@ class BrowserModRegisteredBrowsersCard extends LitElement {
const browserID = ev.currentTarget.browserID; const browserID = ev.currentTarget.browserID;
const unregisterCallback = () => { const unregisterCallback = () => {
console.log(browserID, window.browser_mod.browserID);
if (browserID === window.browser_mod.browserID) { if (browserID === window.browser_mod.browserID) {
console.log("Unregister self");
window.browser_mod.registered = false; window.browser_mod.registered = false;
} else { } else {
window.browser_mod.connection.sendMessage({ window.browser_mod.connection.sendMessage({

View File

@ -81,9 +81,9 @@ export const loadHaForm = async () => {
await loadLoadCardHelpers(); await loadLoadCardHelpers();
const helpers = await window.loadCardHelpers(); const helpers = await window.loadCardHelpers();
if (!helpers) return; if (!helpers) return;
const card = await helpers.createCardElement({ type: "entity" }); const card = await helpers.createCardElement({ type: "button" });
if (!card) return; if (!card) return;
await card.getConfigElement(); await card.constructor.getConfigElement();
}; };
// Loads in ha-config-dashboard which is used to copy styling // Loads in ha-config-dashboard which is used to copy styling
@ -120,3 +120,15 @@ export const loadDeveloperToolsTemplate = async () => {
await dtRouter?.routerOptions?.routes?.template?.load?.(); await dtRouter?.routerOptions?.routes?.template?.load?.();
await customElements.whenDefined("developer-tools-template"); await customElements.whenDefined("developer-tools-template");
}; };
export function throttle(timeout) {
return function (target, propertyKey, descriptor) {
const fn = descriptor.value;
let cooldown = undefined;
descriptor.value = function (...rest) {
if (cooldown) return;
cooldown = setTimeout(() => (cooldown = undefined), timeout);
return fn.bind(this)(...rest);
};
};
}

View File

@ -1,6 +1,5 @@
import "./browser-player"; import "./browser-player";
// import { BrowserModConnection } from "./connection";
import { ConnectionMixin } from "./connection"; import { ConnectionMixin } from "./connection";
import { ScreenSaverMixin } from "./screensaver"; import { ScreenSaverMixin } from "./screensaver";
import { MediaPlayerMixin } from "./mediaPlayer"; import { MediaPlayerMixin } from "./mediaPlayer";
@ -19,7 +18,8 @@ import { BrowserIDMixin } from "./browserID";
/* /*
TODO: TODO:
- Fix nomenclature - More pictures for documentation
x Fix nomenclature
x Command -> Service x Command -> Service
x Device -> Browser x Device -> Browser
- Popups - Popups
@ -28,6 +28,8 @@ import { BrowserIDMixin } from "./browserID";
X Timeout X Timeout
X Fullscreen X Fullscreen
x Popup-card x Popup-card
x Auto-close
x Forms that are forwarded to service calls
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
@ -39,29 +41,28 @@ import { BrowserIDMixin } from "./browserID";
x ll-custom handling x ll-custom handling
- Commands - Commands
x popup x popup
x Auto-close
x close_popup x close_popup
x more-info x more-info
x navigate x navigate
- lovelace-reload? o lovelace-reload?
- Not needed o Not needed
x window-reload x window-reload
- screensaver ? o screensaver ?
- Refer to automations instead o Refer to automations instead
x sequence x sequence
x delay x delay
x javascript eval x javascript eval
- toast? o toast?
- Replaced with popups with timeout o Replaced with popups with timeout
x Redesign services to target devices x Redesign services to target devices
x frontend editor for popup cards x frontend editor for popup cards
- also screensavers o also screensavers
- Saved frontend settings x Saved frontend settings
X Framework X Framework
x Save sidebar x Save sidebar
x Kiosk mode x Kiosk mode
x Default dashboard x Default dashboard
- Screensaver? o Screensaver?
x Favicon templates x Favicon templates
x Title templates x Title templates
- Tweaks - Tweaks
@ -69,9 +70,9 @@ import { BrowserIDMixin } from "./browserID";
x Card-mod preload x Card-mod preload
x Video player x Video player
x Media_seek x Media_seek
- Screensavers o Screensavers
x IMPORTANT: FIX DEFAULT HIDING OF ENTITIES x IMPORTANT: FIX DEFAULT HIDING OF ENTITIES
- NOFIX. Home Assistant bug o NOFIX. Home Assistant bug
X Check functionality with CAST - may need to add frontend part as a lovelace resource X Check functionality with CAST - may need to add frontend part as a lovelace resource
*/ */
export class BrowserMod extends ServicesMixin( export class BrowserMod extends ServicesMixin(

View File

@ -1,12 +1,11 @@
import { selectTree } from "../helpers"; import { selectTree, throttle } from "../helpers";
export const MediaPlayerMixin = (SuperClass) => { export const MediaPlayerMixin = (SuperClass) => {
return class MediaPlayerMixinClass extends SuperClass { class MediaPlayerMixinClass extends SuperClass {
public player; public player;
private _audio_player; private _audio_player;
private _video_player; private _video_player;
private _player_enabled; private _player_enabled;
private _player_update_cooldown;
constructor() { constructor() {
super(); super();
@ -24,10 +23,10 @@ export const MediaPlayerMixin = (SuperClass) => {
} }
for (const ev of ["timeupdate"]) { for (const ev of ["timeupdate"]) {
this._audio_player.addEventListener(ev, () => this._audio_player.addEventListener(ev, () =>
this._player_update_choked() this._player_update_throttled()
); );
this._video_player.addEventListener(ev, () => this._video_player.addEventListener(ev, () =>
this._player_update_choked() this._player_update_throttled()
); );
} }
@ -99,12 +98,8 @@ export const MediaPlayerMixin = (SuperClass) => {
} }
} }
private _player_update_choked() { @throttle(3000)
if (this._player_update_cooldown) return; _player_update_throttled() {
this._player_update_cooldown = window.setTimeout(
() => (this._player_update_cooldown = undefined),
3000
);
this._player_update(); this._player_update();
} }
@ -129,5 +124,7 @@ export const MediaPlayerMixin = (SuperClass) => {
}, },
}); });
} }
}; }
return MediaPlayerMixinClass;
}; };

View File

@ -2,6 +2,7 @@ import { LitElement, html, css } from "lit";
import { property, query } from "lit/decorators.js"; import { property, query } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { provideHass, loadLoadCardHelpers, hass_base_el } from "../helpers"; import { provideHass, loadLoadCardHelpers, hass_base_el } from "../helpers";
import { loadHaForm } from "../helpers";
class BrowserModPopup extends LitElement { class BrowserModPopup extends LitElement {
@property() open; @property() open;
@ -23,6 +24,7 @@ class BrowserModPopup extends LitElement {
_timeoutStart; _timeoutStart;
_timeoutTimer; _timeoutTimer;
_resolveClosed; _resolveClosed;
_formdata;
async closeDialog() { async closeDialog() {
this.open = false; this.open = false;
@ -76,10 +78,30 @@ class BrowserModPopup extends LitElement {
autoclose = false, autoclose = false,
} = {} } = {}
) { ) {
this._formdata = undefined;
this.title = title; this.title = title;
if (content && content instanceof HTMLElement) {
this.card = undefined; this.card = undefined;
if (content && content instanceof HTMLElement) {
this.content = content; this.content = content;
} else if (content && Array.isArray(content)) {
loadHaForm();
const form: any = document.createElement("ha-form");
form.schema = content;
form.computeLabel = (s) => s.label ?? s.name;
form.hass = window.browser_mod.hass;
this._formdata = {};
for (const i of content) {
if (i.name && i.default !== undefined) {
this._formdata[i.name] = i.default;
}
}
form.data = this._formdata;
provideHass(form);
form.addEventListener("value-changed", (ev) => {
this._formdata = { ...ev.detail.value };
form.data = this._formdata;
});
this.content = form;
} else if (content && typeof content === "object") { } else if (content && typeof content === "object") {
// Create a card from config in content // Create a card from config in content
this.card = true; this.card = true;
@ -90,7 +112,6 @@ class BrowserModPopup extends LitElement {
this.content = card; this.content = card;
} else { } else {
// Basic HTML content // Basic HTML content
this.card = undefined;
this.content = unsafeHTML(content); this.content = unsafeHTML(content);
} }
@ -115,12 +136,12 @@ class BrowserModPopup extends LitElement {
async _primary() { async _primary() {
if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined; if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined;
this.dialog?.close(); this.dialog?.close();
this._actions?.right_button_action?.(); this._actions?.right_button_action?.(this._formdata);
} }
async _secondary() { async _secondary() {
if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined; if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined;
this.dialog?.close(); this.dialog?.close();
this._actions?.left_button_action?.(); this._actions?.left_button_action?.(this._formdata);
} }
async _dismiss(ev?) { async _dismiss(ev?) {
this.dialog?.close(); this.dialog?.close();
@ -150,16 +171,21 @@ class BrowserModPopup extends LitElement {
: ""} : ""}
${this.title ${this.title
? html` ? html`
<app-toolbar slot="heading"> <div slot="heading">
<ha-header-bar>
${this.dismissable ${this.dismissable
? html` ? html`
<ha-icon-button dialogAction="cancel"> <ha-icon-button
dialogAction="cancel"
slot="navigationIcon"
>
<ha-icon .icon=${"mdi:close"}></ha-icon> <ha-icon .icon=${"mdi:close"}></ha-icon>
</ha-icon-button> </ha-icon-button>
` `
: ""} : ""}
<div class="main-title">${this.title}</div> <div slot="title" class="main-title">${this.title}</div>
</app-toolbar> </ha-header-bar>
</div>
` `
: html``} : html``}
@ -171,6 +197,7 @@ class BrowserModPopup extends LitElement {
slot="primaryAction" slot="primaryAction"
.label=${this.right_button} .label=${this.right_button}
@click=${this._primary} @click=${this._primary}
class="action-button"
></mwc-button> ></mwc-button>
` `
: ""} : ""}
@ -180,6 +207,7 @@ class BrowserModPopup extends LitElement {
slot="secondaryAction" slot="secondaryAction"
.label=${this.left_button} .label=${this.left_button}
@click=${this._secondary} @click=${this._secondary}
class="action-button"
></mwc-button> ></mwc-button>
` `
: ""} : ""}
@ -195,6 +223,7 @@ class BrowserModPopup extends LitElement {
static get styles() { static get styles() {
return css` return css`
ha-dialog { ha-dialog {
--dialog-backdrop-filter: blur(5px);
z-index: 10; z-index: 10;
--mdc-dialog-min-width: var(--popup-min-width, 400px); --mdc-dialog-min-width: var(--popup-min-width, 400px);
--mdc-dialog-max-width: var(--popup-max-width, 600px); --mdc-dialog-max-width: var(--popup-max-width, 600px);
@ -202,13 +231,12 @@ class BrowserModPopup extends LitElement {
--mdc-dialog-content-ink-color: var(--primary-text-color); --mdc-dialog-content-ink-color: var(--primary-text-color);
--justify-action-buttons: space-between; --justify-action-buttons: space-between;
--mdc-dialog-box-shadow: 0px 0px 0px --dialog-box-shadow: 0px 0px 0px
var(--popup-border-width, var(--ha-card-border-width, 2px)) var(--popup-border-width, var(--ha-card-border-width, 2px))
var( var(
--popup-border-color, --popup-border-color,
var(--ha-card-border-color, var(--divider-color, #e0e0e0)) var(--ha-card-border-color, var(--divider-color, #e0e0e0))
); );
--ha-dialog-border-radius: var(--popup-border-radius, 8px);
--mdc-theme-surface: var( --mdc-theme-surface: var(
--popup-background-color, --popup-background-color,
var(--ha-card-background, var(--card-background-color, white)) var(--ha-card-background, var(--card-background-color, white))
@ -224,6 +252,7 @@ class BrowserModPopup extends LitElement {
--mdc-dialog-max-height: 100%; --mdc-dialog-max-height: 100%;
--mdc-shape-medium: 0px; --mdc-shape-medium: 0px;
--vertial-align-dialog: flex-end; --vertial-align-dialog: flex-end;
--ha-dialog-border-radius: 0px;
} }
.progress::before { .progress::before {
content: ""; content: "";
@ -236,23 +265,20 @@ class BrowserModPopup extends LitElement {
z-index: 10; z-index: 10;
} }
app-toolbar { ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0; flex-shrink: 0;
color: var(--primary-text-color); display: block;
background-color: var(
--popup-header-background-color,
var(--popup-background-color, var(--sidebar-background-color))
);
} }
ha-icon-button > * { ha-icon-button > * {
display: flex; display: flex;
} }
.main-title { .main-title {
margin-left: 16px;
line-height: 1.3em;
max-height: 2.6em;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
cursor: default;
} }
.content { .content {
--padding-x: 24px; --padding-x: 24px;
@ -269,9 +295,11 @@ class BrowserModPopup extends LitElement {
:host([card]) .content { :host([card]) .content {
--padding-x: 0px; --padding-x: 0px;
--padding-y: 0px; --padding-y: 0px;
--ha-card-box-shadow: none;
} }
:host([actions]) .content { :host([actions]) .content {
border-bottom: 1px solid var(--popup-border-color, var(--divider-color)); xborder-bottom: 2px solid
var(--popup-border-color, var(--divider-color));
--footer-height: 54px; --footer-height: 54px;
} }
:host([wide]) .content { :host([wide]) .content {
@ -280,10 +308,14 @@ class BrowserModPopup extends LitElement {
:host([fullscreen]) .content { :host([fullscreen]) .content {
height: calc( height: calc(
100vh - var(--header-height) - var(--footer-height) - 2 * 100vh - var(--header-height) - var(--footer-height) - 2 *
var(--padding-y) var(--padding-y) - 16px
); );
} }
.action-button {
margin-bottom: -24px;
}
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog { ha-dialog {
--mdc-dialog-min-width: 100vw; --mdc-dialog-min-width: 100vw;

View File

@ -128,7 +128,13 @@ export const ServicesMixin = (SuperClass) => {
const { title, content, ...d } = data; const { title, content, ...d } = data;
for (const [k, v] of Object.entries(d)) { for (const [k, v] of Object.entries(d)) {
if (k.endsWith("_action")) { if (k.endsWith("_action")) {
d[k] = () => this._service_action(v as any); d[k] = (ext_data?) => {
const { service, data } = v as any;
this._service_action({
service,
data: { ...data, ...ext_data },
});
};
} }
} }
this.showPopup(title, content, d); this.showPopup(title, content, d);
@ -147,18 +153,21 @@ export const ServicesMixin = (SuperClass) => {
break; break;
case "console": case "console":
console.log(data.message); if (
Object.keys(data).length > 1 ||
(data && data.message === undefined)
)
console.dir(data);
else console.log(data.message);
break; break;
case "javascript": case "javascript":
const code = ` const code = `
"use strict"; "use strict";
// Insert global definitions here
const hass = (document.querySelector("home-assistant") || document.querySelector("hc-main")).hass;
${data.code} ${data.code}
`; `;
const fn = new Function(code); const fn = new Function("hass", "data", code);
fn(); fn(this.hass, data);
break; break;
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "browser_mod", "name": "browser_mod",
"private": true, "private": true,
"version": "2.0.0b2", "version": "2.0.0b3",
"description": "", "description": "",
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",