import { HomeAssistant } from 'custom-card-helpers'; import { CSSResultGroup, html, LitElement, PropertyValues, TemplateResult, unsafeCSS, } from 'lit'; import { customElement, property } from 'lit/decorators.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 submenuStyle from '../scss/submenu.scss'; import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect, StateParameters, } from '../types.js'; import { frigateCardHasAction, stopEventFromActivatingCardWideActions, } from '../utils/action.js'; import { isHassDifferent, refreshDynamicStateParameters } from '../utils/ha'; import { domainIcon } from '../utils/icons/domain-icon.js'; @customElement('frigate-card-submenu') export class FrigateCardSubmenu extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public submenu?: MenuSubmenu; protected _renderItem(item: MenuSubmenuItem): TemplateResult | void { if (!this.hass) { return; } const stateParameters = refreshDynamicStateParameters(this.hass, { ...item }); const getIcon = (stateParameters: StateParameters): TemplateResult => { if (stateParameters.icon) { return html` `; } return html``; }; return html` { // Attach the action config so ascendants have access to it. ev.detail.config = item; }} .actionHandler=${actionHandler({ hasHold: frigateCardHasAction(item.hold_action), hasDoubleClick: frigateCardHasAction(item.double_tap_action), })} > ${stateParameters.title || ''} ${item.subtitle ? html`${item.subtitle}` : ''} ${getIcon(stateParameters)} `; } protected render(): TemplateResult { if (!this.submenu) { return html``; } return html` ev.stopPropagation() } @click=${(ev) => stopEventFromActivatingCardWideActions(ev)} > trigger slot to open/close the menu. Further // propagation is forbidden by the @click handler on // . allowPropagation: true, hasHold: frigateCardHasAction(this.submenu.hold_action), hasDoubleClick: frigateCardHasAction(this.submenu.double_tap_action), })} > ${this.submenu.items.map(this._renderItem.bind(this))} `; } static get styles(): CSSResultGroup { 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; 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 && isHassDifferent(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({ state_color: true, selected: stateObj.state === option, enabled: true, 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` `; } } declare global { interface HTMLElementTagNameMap { 'frigate-card-submenu': FrigateCardSubmenu; 'frigate-card-submenu-select': FrigateCardSubmenuSelect; } }