Merge pull request #547 from dermotduffy/select-entity-submenu
Add native select/input_select entity support for submenus
This commit is contained in:
@@ -655,6 +655,8 @@ Parameters for the `custom:frigate-card-menu-submenu` element are identical to t
|
|||||||
| `type` | Must be `custom:frigate-card-menu-submenu`. |
|
| `type` | Must be `custom:frigate-card-menu-submenu`. |
|
||||||
| `items` | A list of menu items, as described below. |
|
| `items` | A list of menu items, as described below. |
|
||||||
|
|
||||||
|
<a name="frigate-card-submenu-items"></a>
|
||||||
|
|
||||||
##### Submenu Items
|
##### Submenu Items
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
@@ -669,6 +671,19 @@ Parameters for the `custom:frigate-card-menu-submenu` element are identical to t
|
|||||||
|
|
||||||
See the [Configuring a Submenu example](#configuring-a-submenu-example).
|
See the [Configuring a Submenu example](#configuring-a-submenu-example).
|
||||||
|
|
||||||
|
#### `custom:frigate-card-menu-submenu-select`
|
||||||
|
|
||||||
|
This element allows you to easily convert a [Home Assistant Select Entity](https://www.home-assistant.io/integrations/select/) or [Home Assistant Input Select Entity](https://www.home-assistant.io/integrations/input_select/) (an entity either starting with `select` or `input_select`) into an overridable submenu. This *could* be done by hand using a regular submenu (above) -- this element is a convenience.
|
||||||
|
|
||||||
|
Parameters for the `custom:frigate-card-menu-submenu-select` element are identical to the parameters of the [stock Home Assistant State Icon Element](https://www.home-assistant.io/dashboards/picture-elements/#state-icon) with the exception of these parameters which differ:
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| - | - |
|
||||||
|
| `type` | Must be `custom:frigate-card-menu-submenu-select`. |
|
||||||
|
| `options` | An optional dictionary of overrides keyed by the option name that the given select entity supports. These options can be used to set or override submenu item parameters on a per-option basis. The format is as described in [Submenu Items](#frigate-card-submenu-items) above.|
|
||||||
|
|
||||||
|
See the [Configuring a Select Submenu example](#configuring-a-select-submenu-example).
|
||||||
|
|
||||||
<a name="frigate-card-conditional"></a>
|
<a name="frigate-card-conditional"></a>
|
||||||
|
|
||||||
#### `custom:frigate-card-conditional`
|
#### `custom:frigate-card-conditional`
|
||||||
@@ -1210,6 +1225,53 @@ elements:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<a name="configuring-a-select-submenu-example"></a>
|
||||||
|
|
||||||
|
### Configuring a `select` submenu
|
||||||
|
|
||||||
|
You can easily add a submenu to the menu based on a `select` or `input_select` entity.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Adding a select submenu</summary>
|
||||||
|
|
||||||
|
This example imagines the user has an `input_select` entity configured in their Home Assistant configuration like so:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
input_select:
|
||||||
|
kitchen_scene:
|
||||||
|
name: Kitchen Scene Select
|
||||||
|
options:
|
||||||
|
- scene.kitchen_cooking_scene
|
||||||
|
- scene.kitchen_tv_scene
|
||||||
|
icon: mdi:lightbulb
|
||||||
|
```
|
||||||
|
|
||||||
|
The following will convert this entity into a submenu:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
[...]
|
||||||
|
elements:
|
||||||
|
- type: custom:frigate-card-menu-submenu-select
|
||||||
|
entity: input_select.kitchen_scene
|
||||||
|
```
|
||||||
|
|
||||||
|
To override 1 or more individual options (e.g. to set custom icons and titles)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
[...]
|
||||||
|
elements:
|
||||||
|
- type: custom:frigate-card-menu-submenu-select
|
||||||
|
icon: mdi:lamps
|
||||||
|
entity: input_select.kitchen_scene
|
||||||
|
options:
|
||||||
|
scene.kitchen_cooking_scene:
|
||||||
|
icon: mdi:chef-hat
|
||||||
|
title: Cooking time!
|
||||||
|
scene.kitchen_tv_scene:
|
||||||
|
icon: mdi:television
|
||||||
|
title: TV!
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### Overriding card behavior
|
### Overriding card behavior
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -9,7 +9,8 @@ import styles from 'rollup-plugin-styles';
|
|||||||
import image from '@rollup/plugin-image';
|
import image from '@rollup/plugin-image';
|
||||||
import replace from '@rollup/plugin-replace';
|
import replace from '@rollup/plugin-replace';
|
||||||
|
|
||||||
const dev = process.env.ROLLUP_WATCH === 'true' || process.env.ROLLUP_WATCH === '1';
|
const watch = process.env.ROLLUP_WATCH === 'true' || process.env.ROLLUP_WATCH === '1';
|
||||||
|
const dev = watch || process.env.DEV === 'true' || process.env.DEV === '1';
|
||||||
|
|
||||||
const serveopts = {
|
const serveopts = {
|
||||||
contentBase: ['./dist'],
|
contentBase: ['./dist'],
|
||||||
@@ -50,7 +51,7 @@ const plugins = [
|
|||||||
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'),
|
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
dev && serve(serveopts),
|
watch && serve(serveopts),
|
||||||
!dev && terser(),
|
!dev && terser(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
MenuStateIcon,
|
MenuStateIcon,
|
||||||
PictureElements,
|
PictureElements,
|
||||||
MenuSubmenu,
|
MenuSubmenu,
|
||||||
|
MenuSubmenuSelect,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js';
|
import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js';
|
||||||
|
|
||||||
@@ -331,3 +332,6 @@ export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMen
|
|||||||
|
|
||||||
@customElement('frigate-card-menu-submenu')
|
@customElement('frigate-card-menu-submenu')
|
||||||
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
|
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
|
||||||
|
|
||||||
|
@customElement('frigate-card-menu-submenu-select')
|
||||||
|
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
|
||||||
|
|||||||
@@ -252,13 +252,20 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
if (button.enabled === false) {
|
if (button.enabled === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (button.type == 'custom:frigate-card-menu-submenu') {
|
if (button.type === 'custom:frigate-card-menu-submenu') {
|
||||||
return html` <frigate-card-submenu
|
return html` <frigate-card-submenu
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.submenu=${button}
|
.submenu=${button}
|
||||||
@action=${this._actionHandler.bind(this)}
|
@action=${this._actionHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-submenu>`;
|
</frigate-card-submenu>`;
|
||||||
|
} else if (button.type === 'custom:frigate-card-menu-submenu-select') {
|
||||||
|
return html` <frigate-card-submenu-select
|
||||||
|
.hass=${this.hass}
|
||||||
|
.submenuSelect=${button}
|
||||||
|
@action=${this._actionHandler.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-submenu-select>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stateParameters: StateParameters = { ...button };
|
let stateParameters: StateParameters = { ...button };
|
||||||
|
|||||||
+118
-6
@@ -1,17 +1,27 @@
|
|||||||
import type { Corner } from '@material/mwc-menu';
|
import type { Corner } from '@material/mwc-menu';
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
unsafeCSS,
|
||||||
|
} from 'lit';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
refreshDynamicStateParameters,
|
refreshDynamicStateParameters,
|
||||||
|
shouldUpdateBasedOnHass,
|
||||||
stopEventFromActivatingCardWideActions,
|
stopEventFromActivatingCardWideActions,
|
||||||
} from '../common.js';
|
} from '../common.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { MenuSubmenu, MenuSubmenuItem } from '../types.js';
|
import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../types.js';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
|
import { domainIcon } from '../icons/domain-icon.js';
|
||||||
|
|
||||||
import submenuStyle from '../scss/submenu.scss';
|
import submenuStyle from '../scss/submenu.scss';
|
||||||
|
|
||||||
@@ -35,7 +45,7 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
style="${styleMap(stateParameters.style || {})}"
|
style="${styleMap(stateParameters.style || {})}"
|
||||||
graphic="icon"
|
graphic=${ifDefined(stateParameters.icon ? 'icon' : undefined)}
|
||||||
?twoline=${!!item.subtitle}
|
?twoline=${!!item.subtitle}
|
||||||
?selected=${item.selected}
|
?selected=${item.selected}
|
||||||
?activated=${item.selected}
|
?activated=${item.selected}
|
||||||
@@ -51,9 +61,7 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>${stateParameters.title || ''}</span>
|
<span>${stateParameters.title || ''}</span>
|
||||||
${item.subtitle
|
${item.subtitle ? html`<span slot="secondary">${item.subtitle}</span>` : ''}
|
||||||
? html`<span slot="secondary">${item.subtitle}</span>`
|
|
||||||
: ''}
|
|
||||||
${stateParameters.icon
|
${stateParameters.icon
|
||||||
? html` <ha-icon
|
? html` <ha-icon
|
||||||
data-domain=${ifDefined(stateParameters.data_domain)}
|
data-domain=${ifDefined(stateParameters.data_domain)}
|
||||||
@@ -109,3 +117,107 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
return unsafeCSS(submenuStyle);
|
return unsafeCSS(submenuStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-submenu-select')
|
||||||
|
export class FrigateCardSubmenuSelect extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public submenuSelect?: MenuSubmenuSelect;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public corner?: Corner;
|
||||||
|
|
||||||
|
protected _generatedSubmenu?: MenuSubmenu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to determine if the update should proceed.
|
||||||
|
* @param changedProps
|
||||||
|
* @returns `true` if the update should proceed, `false` otherwise.
|
||||||
|
*/
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
// No need to update the submenu unless the select entity has changed.
|
||||||
|
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
||||||
|
return (
|
||||||
|
changedProps.size != 1 ||
|
||||||
|
!this.submenuSelect ||
|
||||||
|
(!!oldHass &&
|
||||||
|
shouldUpdateBasedOnHass(this.hass, oldHass, [this.submenuSelect.entity]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the render function will be called.
|
||||||
|
*/
|
||||||
|
protected willUpdate(): void {
|
||||||
|
if (!this.submenuSelect || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entity = this.submenuSelect.entity;
|
||||||
|
const stateObj = this.hass.states[entity];
|
||||||
|
const options = stateObj?.attributes?.options;
|
||||||
|
if (!stateObj || !options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submenu: MenuSubmenu = {
|
||||||
|
// Default icon. It should be impossible for this to be used, since
|
||||||
|
// this.submenuSelect will always have an entity, which means
|
||||||
|
// refreshDynamicStateParameters will always return an icon.
|
||||||
|
icon: domainIcon('select'),
|
||||||
|
|
||||||
|
// Pull out the dynamic properties (like icon, and title) from the state.
|
||||||
|
...refreshDynamicStateParameters(this.hass, this.submenuSelect),
|
||||||
|
|
||||||
|
// Override it with anything explicitly set in the submenuSelect.
|
||||||
|
...this.submenuSelect,
|
||||||
|
|
||||||
|
type: 'custom:frigate-card-menu-submenu',
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// For cleanliness remove the options parameter which is unused by the
|
||||||
|
// submenu rendering itself (above). It is only in this method to populate
|
||||||
|
// the items correctly (below).
|
||||||
|
delete submenu['options'];
|
||||||
|
|
||||||
|
for (const option of options) {
|
||||||
|
// If there's a device_class there may be a localized translation of the
|
||||||
|
// select title available via HASS.
|
||||||
|
const title = stateObj.attributes.device_class
|
||||||
|
? this.hass.localize(
|
||||||
|
`component.select.state.${stateObj.attributes.device_class}.${option}`,
|
||||||
|
)
|
||||||
|
: option;
|
||||||
|
submenu.items.push({
|
||||||
|
selected: stateObj.state === option,
|
||||||
|
title: title || option,
|
||||||
|
...((entity.startsWith('select.') || entity.startsWith('input_select.')) && {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: entity.startsWith('select.')
|
||||||
|
? 'select.select_option'
|
||||||
|
: 'input_select.select_option',
|
||||||
|
service_data: {
|
||||||
|
entity_id: entity,
|
||||||
|
option: option,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Apply overrides the user may have specified for a given option.
|
||||||
|
...(this.submenuSelect.options && this.submenuSelect.options[option]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._generatedSubmenu = submenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html` <frigate-card-submenu
|
||||||
|
.hass=${this.hass}
|
||||||
|
.corner=${this.corner}
|
||||||
|
.submenu=${this._generatedSubmenu}
|
||||||
|
></frigate-card-submenu>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-2
@@ -436,12 +436,19 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
|
|||||||
});
|
});
|
||||||
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
|
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
|
||||||
|
|
||||||
export const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({
|
const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({
|
||||||
type: z.literal('custom:frigate-card-menu-submenu'),
|
type: z.literal('custom:frigate-card-menu-submenu'),
|
||||||
items: menuSubmenuItemSchema.array(),
|
items: menuSubmenuItemSchema.array(),
|
||||||
});
|
});
|
||||||
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
|
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
|
||||||
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu;
|
|
||||||
|
const menuSubmenuSelectSchema = menuBaseSchema.merge(stateIconSchema).extend({
|
||||||
|
type: z.literal('custom:frigate-card-menu-submenu-select'),
|
||||||
|
options: z.record(menuSubmenuItemSchema).optional(),
|
||||||
|
});
|
||||||
|
export type MenuSubmenuSelect = z.infer<typeof menuSubmenuSelectSchema>;
|
||||||
|
|
||||||
|
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelect;
|
||||||
|
|
||||||
const frigateCardConditionSchema = z.object({
|
const frigateCardConditionSchema = z.object({
|
||||||
view: z.string().array().optional(),
|
view: z.string().array().optional(),
|
||||||
@@ -464,6 +471,7 @@ const pictureElementSchema = z.union([
|
|||||||
menuStateIconSchema,
|
menuStateIconSchema,
|
||||||
menuIconSchema,
|
menuIconSchema,
|
||||||
menuSubmenuSchema,
|
menuSubmenuSchema,
|
||||||
|
menuSubmenuSelectSchema,
|
||||||
frigateConditionalSchema,
|
frigateConditionalSchema,
|
||||||
stateBadgeIconSchema,
|
stateBadgeIconSchema,
|
||||||
stateIconSchema,
|
stateIconSchema,
|
||||||
@@ -1072,6 +1080,7 @@ const menuButtonSchema = z.discriminatedUnion('type', [
|
|||||||
menuIconSchema,
|
menuIconSchema,
|
||||||
menuStateIconSchema,
|
menuStateIconSchema,
|
||||||
menuSubmenuSchema,
|
menuSubmenuSchema,
|
||||||
|
menuSubmenuSelectSchema,
|
||||||
]);
|
]);
|
||||||
export type MenuButton = z.infer<typeof menuButtonSchema>;
|
export type MenuButton = z.infer<typeof menuButtonSchema>;
|
||||||
export interface ExtendedHomeAssistant extends HomeAssistant {
|
export interface ExtendedHomeAssistant extends HomeAssistant {
|
||||||
|
|||||||
Reference in New Issue
Block a user