Files
advanced-camera-card/src/components-lib/menu-controller.ts
T

184 lines
5.3 KiB
TypeScript

import { HASSDomEvent } from '@dermotduffy/custom-card-helpers';
import { LitElement } from 'lit';
import { orderBy } from 'lodash-es';
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request.js';
import {
FRIGATE_MENU_PRIORITY_MAX,
type ActionType,
type ActionsConfig,
type MenuConfig,
type MenuItem,
} from '../config/types.js';
import {
convertActionToCardCustomAction,
getActionConfigGivenAction,
} from '../utils/action';
import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js';
export class MenuController {
protected _host: LitElement;
protected _config: MenuConfig | null = null;
protected _buttons: MenuItem[] = [];
protected _expanded = false;
constructor(host: LitElement) {
this._host = host;
}
public setMenuConfig(config: MenuConfig): void {
this._config = config;
this._host.style.setProperty(
'--frigate-card-menu-button-size',
`${config.button_size}px`,
);
// Store the menu style, position and alignment as attributes (used for
// styling).
this._host.setAttribute('data-style', config.style);
this._host.setAttribute('data-position', config.position);
this._host.setAttribute('data-alignment', config.alignment);
this._sortButtons();
this._host.requestUpdate();
}
public getMenuConfig(): MenuConfig | null {
return this._config;
}
public isExpanded(): boolean {
return this._expanded;
}
public setButtons(buttons: MenuItem[]): void {
this._buttons = buttons;
this._sortButtons();
this._host.requestUpdate();
}
public getButtons(alignment: 'matching' | 'opposing'): MenuItem[] {
const aligned = (button: MenuItem): boolean => {
return (
button.alignment === alignment || (alignment === 'matching' && !button.alignment)
);
};
const enabled = (button: MenuItem): boolean => {
return button.enabled !== false;
};
const show = (button: MenuItem): boolean => {
return !this._isHidingMenu() || this._expanded || !!button.permanent;
};
return this._buttons.filter(
(button) => enabled(button) && aligned(button) && show(button),
);
}
public setExpanded(expanded: boolean): void {
this._expanded = expanded;
setOrRemoveAttribute(this._host, expanded, 'expanded');
this._host.requestUpdate();
}
public toggleExpanded(): void {
this.setExpanded(!this._expanded);
}
public actionHandler(
ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
config?: ActionsConfig,
): void {
// These interactions should only be handled by the menu, as nothing
// upstream has the user-provided configuration.
ev.stopPropagation();
// 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;
}
if (!config) {
return;
}
const interaction: string = ev.detail.action;
const action = getActionConfigGivenAction(interaction, config);
if (!action) {
return;
}
const actions = arrayify(action);
// A note on the complexity below: By default the menu should close when a
// user takes an action, an exception is if the user is specifically
// manipulating the menu in the actions themselves.
let menuToggle = false;
const toggleLessActions = actions.filter(
(item) => isTruthy(item) && !this._isUnknownActionMenuToggleAction(item),
);
if (toggleLessActions.length != actions.length) {
menuToggle = true;
}
if (toggleLessActions.length) {
dispatchActionExecutionRequest(this._host, {
action: actions,
config: config,
});
}
if (this._isHidingMenu()) {
if (menuToggle) {
this.setExpanded(!this._expanded);
} else {
// Don't close the menu if there is another action to come.
const holdAction = getActionConfigGivenAction('hold', config);
const doubleTapAction = getActionConfigGivenAction('double_tap', config);
const tapAction = getActionConfigGivenAction('tap', config);
const endTapAction = getActionConfigGivenAction('end_tap', config);
if (
interaction === 'end_tap' ||
(interaction === 'start_tap' &&
!holdAction &&
!doubleTapAction &&
!tapAction &&
!endTapAction) ||
(interaction !== 'end_tap' && !endTapAction)
) {
this.setExpanded(false);
}
}
}
}
protected _sortButtons(): void {
this._buttons = orderBy(
this._buttons,
(button) => {
const priority = button.priority ?? 0;
// If the menu is hidden, the buttons that toggle the menu must come
// first.
return (
priority +
(this._isHidingMenu() && button.permanent ? FRIGATE_MENU_PRIORITY_MAX : 0)
);
},
['desc'],
);
}
protected _isHidingMenu(): boolean {
return this._config?.style === 'hidden';
}
protected _isUnknownActionMenuToggleAction(action: ActionType): boolean {
const parsedAction = convertActionToCardCustomAction(action);
return !!parsedAction && parsedAction.frigate_card_action == 'menu_toggle';
}
}