import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../action-handler-directive.js';
import menuStyle from '../scss/menu.scss';
import type {
ActionsConfig,
ActionType,
MenuButton,
MenuConfig,
MenuItem,
StateParameters
} from '../types.js';
import {
convertActionToFrigateCardCustomAction,
frigateCardHandleActionConfig,
frigateCardHasAction,
getActionConfigGivenAction
} from '../utils/action.js';
import { FRIGATE_ICON_SVG_PATH } from '../camera/frigate/icon.js';
import { refreshDynamicStateParameters } from '../utils/ha';
import './submenu.js';
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
/**
* A menu for the FrigateCard.
*/
@customElement('frigate-card-menu')
export class FrigateCardMenu extends LitElement {
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: true, type: Boolean, reflect: true })
public expanded = false;
set menuConfig(menuConfig: MenuConfig) {
this._menuConfig = menuConfig;
if (menuConfig) {
this.style.setProperty(
'--frigate-card-menu-button-size',
`${menuConfig.button_size}px`,
);
}
// Store the menu style, position and alignment as attributes (used for
// styling).
this.setAttribute('data-style', menuConfig.style);
this.setAttribute('data-position', menuConfig.position);
this.setAttribute('data-alignment', menuConfig.alignment);
}
@state()
protected _menuConfig?: MenuConfig;
@property({ attribute: false })
public buttons: MenuButton[] = [];
/**
* Determine if a given menu configuration is a hiding menu.
* @param menuConfig The menu configuration.
* @returns `true` if the menu is hiding, `false` otherwise.
*/
static isHidingMenu(menuConfig: MenuConfig | undefined): boolean {
return menuConfig?.style === 'hidden' ?? false;
}
/**
* Toggle the menu. Has no action if menu is not hiding/expandable.
*/
public toggleMenu(): void {
if (this._isHidingMenu()) {
this.expanded = !this.expanded;
}
}
/**
* Determine if a given menu configuration is a hiding menu (internal version).
* @returns `true` if the menu is hiding, `false` otherwise.
*/
protected _isHidingMenu(): boolean {
return FrigateCardMenu.isHidingMenu(this._menuConfig);
}
/**
* Determine if a given action is intended to toggle the menu.
* @param action The action to check.
* @returns `true` if the action toggles the menu, `false` otherwise.
*/
protected _isMenuToggleAction(action: ActionType | undefined): boolean {
// Determine if this action is a Frigate card action, if so handle it
// internally.
if (!action) {
return false;
}
const frigateCardAction = convertActionToFrigateCardCustomAction(action);
return !!frigateCardAction && frigateCardAction.frigate_card_action == 'menu_toggle';
}
/**
* Handle an action on a menu button.
* @param ev The action event.
* @param button The button configuration.
*/
protected _actionHandler(
ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
config?: ActionsConfig,
): 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 menu, as nothing
// upstream has the user-provided configuration.
ev.stopPropagation();
const interaction: string = ev.detail.action;
let action = getActionConfigGivenAction(interaction, config);
if (!config || !interaction) {
return;
}
let tookAction = false;
let menuToggle = false;
if (Array.isArray(action)) {
// Case 1: An array of actions.
// Strip out actions that toggle the menu.
const actionCount = action.length;
action = action.filter((item) => !this._isMenuToggleAction(item));
if (action.length != actionCount) {
menuToggle = true;
}
// If there are still actions left, handle them as usual.
if (action.length) {
tookAction = frigateCardHandleActionConfig(
this,
this.hass as HomeAssistant,
config,
interaction,
action,
);
}
} else {
// Case 2: Either a specific action, or no action at all (i.e. default
// action for `tap`).
if (this._isMenuToggleAction(action)) {
menuToggle = true;
} else {
tookAction = frigateCardHandleActionConfig(
this,
this.hass as HomeAssistant,
config,
interaction,
action,
);
}
}
if (this._isHidingMenu()) {
if (menuToggle) {
this.expanded = !this.expanded;
} else if (tookAction) {
this.expanded = false;
}
}
}
/**
* Ensure menu buttons are sorted before the render.
* @param changedProps The changed properties
*/
protected willUpdate(changedProps: PropertyValues): void {
const style = this._menuConfig?.style;
const sortButtons = (a: MenuItem, b: MenuItem): number => {
// If the menu is hidden, the Frigate button must come first.
if (style === 'hidden') {
if (a.icon === FRIGATE_BUTTON_MENU_ICON) {
return -1;
} else if (b.icon === FRIGATE_BUTTON_MENU_ICON) {
return 1;
}
}
// Otherwise sort by priority.
if (
a.priority === undefined ||
(b.priority !== undefined && b.priority > a.priority)
) {
return 1;
}
if (
b.priority === undefined ||
(a.priority !== undefined && b.priority < a.priority)
) {
return -1;
}
return 0;
};
if (changedProps.has('_menuConfig') || changedProps.has('buttons')) {
this.buttons.sort(sortButtons);
}
}
/**
* Render a button.
* @param button The button configuration to render.
* @returns A rendered template or void.
*/
protected _renderButton(button: MenuButton): TemplateResult | void {
if (button.enabled === false) {
return;
}
if (button.type === 'custom:frigate-card-menu-submenu') {
return html`