Initial submenu support.
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"dependencies": {
|
||||
"@cycjimmy/jsmpeg-player": "^5.0.1",
|
||||
"@material/image-list": "^12.0.0",
|
||||
"@material/rtl": "^13.0.0",
|
||||
"custom-card-helpers": "^1.8.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"dlv": "github:developit/dlv",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MenuIcon,
|
||||
MenuStateIcon,
|
||||
PictureElements,
|
||||
MenuSubmenu,
|
||||
} from '../types.js';
|
||||
import {
|
||||
dispatchErrorMessageEvent,
|
||||
@@ -339,3 +340,6 @@ export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuIcon
|
||||
|
||||
@customElement('frigate-card-menu-state-icon')
|
||||
export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuIcon<MenuStateIcon> {}
|
||||
|
||||
@customElement('frigate-card-menu-submenu')
|
||||
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
|
||||
+24
-4
@@ -14,7 +14,10 @@ import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
|
||||
import './submenu.js';
|
||||
|
||||
import type {
|
||||
Actions,
|
||||
ExtendedHomeAssistant,
|
||||
MenuButton,
|
||||
MenuConfig,
|
||||
@@ -61,18 +64,26 @@ export class FrigateCardMenu extends LitElement {
|
||||
* @param ev The action event.
|
||||
* @param button The button configuration.
|
||||
*/
|
||||
protected _actionHandler(ev: CustomEvent, button: MenuButton): void {
|
||||
protected _actionHandler(ev: CustomEvent<{action: string, config?: Actions}>, config?: Actions): void {
|
||||
if (!ev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the event itself contains a configuration then use that. This is
|
||||
// useful in cases where the registration of the event handler does not have
|
||||
// access to the actual desired configuration (e.g. action events generated
|
||||
// by a submenu).
|
||||
if (ev.detail.config) {
|
||||
config = ev.detail.config;
|
||||
}
|
||||
|
||||
// These interactions should only be handled by the card, as nothing
|
||||
// upstream has the user-provided configuration.
|
||||
ev.stopPropagation();
|
||||
|
||||
const interaction: string = ev.detail.action;
|
||||
const action = getActionConfigGivenAction(interaction, button);
|
||||
if (!action || !interaction) {
|
||||
const action = getActionConfigGivenAction(interaction, config);
|
||||
if (!config || !action || !interaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,7 +103,7 @@ export class FrigateCardMenu extends LitElement {
|
||||
|
||||
// Collapse menu after the user clicks on something.
|
||||
this.expand = false;
|
||||
handleAction(this, this.hass as HomeAssistant, button, interaction);
|
||||
handleAction(this, this.hass as HomeAssistant, config, interaction);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,6 +180,15 @@ export class FrigateCardMenu extends LitElement {
|
||||
button: true,
|
||||
};
|
||||
|
||||
if (button.type == 'custom:frigate-card-menu-submenu') {
|
||||
return html`
|
||||
<frigate-card-submenu
|
||||
.submenu=${button}
|
||||
@action=${this._actionHandler.bind(this)}
|
||||
>
|
||||
</frigate-card-submenu>`;
|
||||
}
|
||||
|
||||
// TODO: Upon a safe distance from the release of HA 2021.11 these
|
||||
// attributes can be removed from the <ha-icon-button>.
|
||||
// - icon (replaced with the embedded <ha-icon>)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
import { MenuSubmenu } from '../types.js';
|
||||
|
||||
import submenuStyle from '../scss/submenu.scss';
|
||||
import { hasAction } from 'custom-card-helpers';
|
||||
|
||||
@customElement('frigate-card-submenu')
|
||||
export class FrigateCardSubmenu extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public submenu?: MenuSubmenu;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.submenu) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-button-menu corner="BOTTOM_LEFT">
|
||||
<ha-icon-button
|
||||
class="button"
|
||||
slot="trigger"
|
||||
.label=${this.submenu.title || ''}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this.submenu.hold_action),
|
||||
hasDoubleClick: hasAction(this.submenu.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<ha-icon icon="${this.submenu.icon}"></ha-icon>
|
||||
</ha-icon-button>
|
||||
${this.submenu.items.map(
|
||||
(item) => html`
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
aria-label="${item.title || ''}"
|
||||
@action=${(ev) => {
|
||||
// Attach the action config so ascendants have access to it.
|
||||
ev.detail.config = item;
|
||||
}}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(item.hold_action),
|
||||
hasDoubleClick: hasAction(item.double_tap_action),
|
||||
})}
|
||||
>
|
||||
${item.title || ''}
|
||||
${item.icon
|
||||
? html` <ha-icon slot="graphic" icon="${item.icon}"> </ha-icon>`
|
||||
: ``}
|
||||
</mwc-list-item>
|
||||
`,
|
||||
)}
|
||||
</ha-button-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(submenuStyle);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -61,7 +61,9 @@ ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
|
||||
// Some elements (such as menus) may need to extend beyond the card boundary.
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
+39
-3
@@ -8,35 +8,71 @@
|
||||
|
||||
.frigate-card-menu {
|
||||
z-index: 1;
|
||||
height: calc(var(--frigate-card-menu-button-size) + 6px);
|
||||
overflow: hidden;
|
||||
/* Menu div itself does not handle click events. Without this, in overlay
|
||||
mode, the menu div prevents clicking on gallery items 'behind' the overlay.
|
||||
*/
|
||||
pointer-events: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.frigate-card-menu.overlay-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: calc(var(--frigate-card-menu-button-size) + 6px);
|
||||
height: calc(var(--frigate-card-menu-button-size) + 6px);
|
||||
}
|
||||
.frigate-card-menu.overlay-hidden.left, .frigate-card-menu.overlay-hidden.top {
|
||||
.frigate-card-menu.overlay-hidden.left {
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
|
||||
// Awful hack: Flexbox column wrapping doesn't work properly in most major
|
||||
// browsers -- the element boundary does not expand to cover the full wrapped
|
||||
// content as it should. This results in the wrapped 'content' appearing
|
||||
// outside the element background (the linear gradient in this case). The
|
||||
// workaround is to use flex row direction for both rows & columns, and use
|
||||
// vertical-lr/vertical-rl as the writing mode for columns -- resetting
|
||||
// writing-mode for descendant elements.
|
||||
//
|
||||
// For more information see the Chromium bug as an example:
|
||||
// - https://bugs.chromium.org/p/chromium/issues/detail?id=507397
|
||||
writing-mode: vertical-lr;
|
||||
}
|
||||
.frigate-card-menu.overlay-hidden.top {
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
.frigate-card-menu.overlay-hidden.right {
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
|
||||
// See "Awful hack" above.
|
||||
writing-mode: vertical-rl;
|
||||
}
|
||||
.frigate-card-menu.overlay-hidden.left > *,.frigate-card-menu.overlay-hidden.right > * {
|
||||
// See "Awful hack" above.
|
||||
writing-mode: horizontal-tb;
|
||||
}
|
||||
|
||||
.frigate-card-menu.overlay-hidden.bottom {
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
|
||||
// If the menu has more content that allows, "wrap upwards" to keep the
|
||||
// Frigate button in the same place.
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
.frigate-card-menu.overlay-hidden.expanded-horizontal {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
background: linear-gradient(90deg, rgba(0,0,0,0.3), rgba(0,0,0,0));
|
||||
}
|
||||
.frigate-card-menu.overlay-hidden.expanded-vertical {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0));
|
||||
}
|
||||
/* Full above/below menu */
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@use './button.scss';
|
||||
|
||||
:host {
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
}
|
||||
+16
-2
@@ -198,7 +198,7 @@ const serviceCallButtonSchema = elementsBaseSchema.merge(
|
||||
}),
|
||||
);
|
||||
|
||||
// https://www.home-assistant.io/lovelace/picture-elements/#icon
|
||||
// https://www.home-assistant.io/lovelace/picture-elements/#icon-element
|
||||
const iconSchema = elementsBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.literal('icon'),
|
||||
@@ -269,6 +269,15 @@ export const menuStateIconSchema = stateIconSchema.merge(
|
||||
);
|
||||
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
|
||||
|
||||
export const menuSubmenuSchema = iconSchema.merge(
|
||||
z.object({
|
||||
type: z.literal('custom:frigate-card-menu-submenu'),
|
||||
// Menu items don't strictly require their own icon.
|
||||
items: iconSchema.omit({ type: true }).partial({ icon: true }).array(),
|
||||
}),
|
||||
);
|
||||
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
|
||||
|
||||
const frigateCardConditionSchema = z.object({
|
||||
view: z.string().array().optional(),
|
||||
fullscreen: z.boolean().optional(),
|
||||
@@ -285,6 +294,7 @@ export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
|
||||
const pictureElementSchema = z.union([
|
||||
menuStateIconSchema,
|
||||
menuIconSchema,
|
||||
menuSubmenuSchema,
|
||||
frigateConditionalSchema,
|
||||
stateBadgeIconSchema,
|
||||
stateIconSchema,
|
||||
@@ -586,7 +596,11 @@ export const frigateCardConfigDefaults = {
|
||||
event_viewer: viewerConfigDefault,
|
||||
};
|
||||
|
||||
const menuButtonSchema = z.union([menuIconSchema, menuStateIconSchema]);
|
||||
const menuButtonSchema = z.union([
|
||||
menuIconSchema,
|
||||
menuStateIconSchema,
|
||||
menuSubmenuSchema,
|
||||
]);
|
||||
export type MenuButton = z.infer<typeof menuButtonSchema>;
|
||||
export interface ExtendedHomeAssistant {
|
||||
hassUrl(path?): string;
|
||||
|
||||
Reference in New Issue
Block a user