diff --git a/package.json b/package.json index 1b2e1a84..5a077cf0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/elements.ts b/src/components/elements.ts index 4422b4dd..7d336989 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -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 {} + +@customElement('frigate-card-menu-submenu') +export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon {} \ No newline at end of file diff --git a/src/components/menu.ts b/src/components/menu.ts index 0b8bd3ef..7a3a7c08 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -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` + + `; + } + // TODO: Upon a safe distance from the release of HA 2021.11 these // attributes can be removed from the . // - icon (replaced with the embedded ) diff --git a/src/components/submenu.ts b/src/components/submenu.ts new file mode 100644 index 00000000..2a46c063 --- /dev/null +++ b/src/components/submenu.ts @@ -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` + + + + + ${this.submenu.items.map( + (item) => html` + { + // 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` ` + : ``} + + `, + )} + + `; + } + + static get styles(): CSSResultGroup { + return unsafeCSS(submenuStyle); + } +} diff --git a/src/scss/card.scss b/src/scss/card.scss index 8bf73f30..4ef20feb 100644 --- a/src/scss/card.scss +++ b/src/scss/card.scss @@ -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; diff --git a/src/scss/menu.scss b/src/scss/menu.scss index 19bce486..0a76fc14 100644 --- a/src/scss/menu.scss +++ b/src/scss/menu.scss @@ -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 */ diff --git a/src/scss/submenu.scss b/src/scss/submenu.scss new file mode 100644 index 00000000..90aee472 --- /dev/null +++ b/src/scss/submenu.scss @@ -0,0 +1,6 @@ +@use './button.scss'; + +:host { + z-index: 20; + pointer-events: auto; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index b037ddc8..6355ec65 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; +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; + const frigateCardConditionSchema = z.object({ view: z.string().array().optional(), fullscreen: z.boolean().optional(), @@ -285,6 +294,7 @@ export type FrigateConditional = z.infer; 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; export interface ExtendedHomeAssistant { hassUrl(path?): string;