Files
advanced-camera-card/src/card-controller/card-element-manager.ts
T

284 lines
9.4 KiB
TypeScript

import { LitElement, ReactiveControllerHost } from 'lit';
import { ActionEventTarget } from '../action-handler-directive';
import { isCardInPanel } from '../ha/panel';
import { LovelaceCard } from '../ha/types';
import { isBeingCasted } from '../utils/casting';
import { isAncestorInEventPath } from '../utils/event-ancestor';
import { CardMediaReviewEventTarget } from '../utils/review';
import { ViewItem } from '../view/item';
import { ActionExecutionRequestEventTarget } from './actions/utils/execution-request';
import { InitializationAspect } from './initialization-manager';
import { CardElementAPI } from './types';
export type ScrollCallback = () => void;
export type MenuToggleCallback = () => void;
export type CardHTMLElement = LovelaceCard &
LitElement &
ReactiveControllerHost &
ActionEventTarget &
ActionExecutionRequestEventTarget &
CardMediaReviewEventTarget;
export class CardElementManager {
private _api: CardElementAPI;
private _element: CardHTMLElement;
private _scrollCallback: ScrollCallback;
private _menuToggleCallback: MenuToggleCallback;
constructor(
api: CardElementAPI,
element: CardHTMLElement,
scrollCallback: ScrollCallback,
menuToggleCallback: MenuToggleCallback,
) {
this._api = api;
this._element = element;
this._scrollCallback = scrollCallback;
this._menuToggleCallback = menuToggleCallback;
}
public getElement(): HTMLElement {
return this._element;
}
public isConnected(): boolean {
return this._element.isConnected;
}
public scrollReset(): void {
this._scrollCallback();
}
public toggleMenu(): void {
this._menuToggleCallback();
}
public update = (): void => {
this._element.requestUpdate();
};
public hasUpdated(): boolean {
return this._element.hasUpdated;
}
public elementConnected(): void {
// Set initial condition state. Must be done after the element is connected to
// allow callbacks to interact with the card.
this._api.getInteractionManager().initialize();
this._api.getFullscreenManager().initialize();
this._api.getExpandManager().initialize();
this._api.getMediaLoadedInfoManager().initialize();
this._api.getMicrophoneManager().initialize();
this._api.getPIPManager().initialize();
this._api.getKeyboardStateManager().initialize();
this._api.getCallManager().initialize();
// These initializers are called when the config is updated, but on initial
// creation of the card hass is not yet available when the config is first
// loaded.
void this._api.getDefaultManager().initialize();
void this._api.getMediaPlayerManager().initialize();
this._api
.getHASSManager()
.getStateWatcher()
?.subscribe(this.update, [
...(this._api.getConfigManager().getConfig()?.view.render_entities ?? []),
// Refresh the card if media player state changes:
// https://github.com/dermotduffy/advanced-camera-card/issues/881
...(this._api.getMediaPlayerManager().getMediaPlayers() ?? []),
]);
// Whether or not the card is in panel mode on the dashboard.
this._element.toggleAttribute('panel', isCardInPanel(this._element));
this._element.toggleAttribute('casted', isBeingCasted());
this._api.getFullscreenManager().connect();
this._element.addEventListener(
'mousemove',
this._api.getInteractionManager().reportInteraction,
);
this._element.addEventListener(
'wheel',
this._api.getInteractionManager().reportInteraction,
);
this._element.addEventListener(
'touchstart',
this._api.getInteractionManager().reportInteraction,
);
this._element.addEventListener(
'touchmove',
this._api.getInteractionManager().reportInteraction,
);
this._element.addEventListener(
'll-custom',
this._api.getActionsManager().handleCustomActionEvent,
);
this._element.addEventListener(
'action',
this._api.getActionsManager().handleInteractionEvent,
);
this._element.addEventListener(
'action',
this._api.getInteractionManager().reportInteraction,
);
this._element.addEventListener(
'advanced-camera-card:action:execution-request',
this._api.getActionsManager().handleActionExecutionRequestEvent,
);
this._element.addEventListener(
'advanced-camera-card:media:reviewed',
this._handleMediaReviewed,
);
// Listen for HA `navigate` actions.
// See: https://github.com/home-assistant/frontend/blob/273992c8e9c3062c6e49481b6d7d688a07067232/src/common/navigate.ts#L43
window.addEventListener(
'location-changed',
this._api.getQueryStringManager().requestExecution,
);
// Listen for history state changes (i.e. user using the browser
// back/forward controls).
window.addEventListener(
'popstate',
this._api.getQueryStringManager().requestExecution,
);
window.addEventListener(
'advanced-camera-card:editor:diagnostics',
this._editorDiagnosticsHandler,
);
this._api.getConditionStateManager()?.setState({
userAgent: navigator.userAgent,
});
// Manually request query string execute as the card will be
// disconnected/reconnected when dashboard 'tab' changes happen within HA.
this._api.getQueryStringManager().requestExecution();
this._api.getIssueManager().resume();
// A reconnected card (e.g. after HA rebuilt it on restart) won't re-render
// on its own; request one so it re-initializes and shows current state.
this._element.requestUpdate();
}
public elementDisconnected(): void {
this._element.toggleAttribute('panel', false);
this._element.toggleAttribute('tabindex', false);
this._element.toggleAttribute('casted', false);
// Suspend issue evaluation so state changes below (e.g. clearing
// mediaLoadedInfo) don't arm timers while the card is detached. Issue state
// is preserved; evaluation resumes via resume() on reconnect.
this._api.getIssueManager().suspend();
// When the dashboard 'tab' is changed, the media is effectively unloaded.
this._api.getMediaLoadedInfoManager().clear();
this._api.getFullscreenManager().disconnect();
this._api.getPIPManager().uninitialize();
this._api.getKeyboardStateManager().uninitialize();
void this._api.getActionsManager().uninitialize();
this._api.getInteractionManager().uninitialize();
this._api.getDefaultManager().uninitialize();
this._api.getHASSManager().getStateWatcher()?.unsubscribe(this.update);
// Uninitialize cameras and triggers to cause them to reinitialize on
// reconnection, to ensure the state subscription/unsubscription works
// correctly and triggers that changed while detached are picked up.
// Reset trigger state first to stop stale timers and clear condition state.
this._api.getCameraTriggersManager().reset();
this._api.getCallManager().uninitialize();
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
this._api
.getInitializationManager()
.uninitialize(InitializationAspect.INITIAL_TRIGGER);
void this._api.getCameraManager().destroy();
this._element.removeEventListener(
'mousemove',
this._api.getInteractionManager().reportInteraction,
);
this._element.removeEventListener(
'wheel',
this._api.getInteractionManager().reportInteraction,
);
this._element.removeEventListener(
'touchstart',
this._api.getInteractionManager().reportInteraction,
);
this._element.removeEventListener(
'touchmove',
this._api.getInteractionManager().reportInteraction,
);
this._element.removeEventListener(
'll-custom',
this._api.getActionsManager().handleCustomActionEvent,
);
this._element.removeEventListener(
'action',
this._api.getActionsManager().handleInteractionEvent,
);
this._element.removeEventListener(
'action',
this._api.getInteractionManager().reportInteraction,
);
this._element.removeEventListener(
'advanced-camera-card:action:execution-request',
this._api.getActionsManager().handleActionExecutionRequestEvent,
);
this._element.removeEventListener(
'advanced-camera-card:media:reviewed',
this._handleMediaReviewed,
);
window.removeEventListener(
'location-changed',
this._api.getQueryStringManager().requestExecution,
);
window.removeEventListener(
'popstate',
this._api.getQueryStringManager().requestExecution,
);
window.removeEventListener(
'advanced-camera-card:editor:diagnostics',
this._editorDiagnosticsHandler,
);
}
private _handleMediaReviewed = (ev: CustomEvent<ViewItem>): void => {
// If the selected media item has a change of review status, update the card
// (e.g. for the menu).
if (
this._api.getViewManager().getView()?.queryResults?.getSelectedResult() ===
ev.detail
) {
this.update();
}
};
protected _editorDiagnosticsHandler = (ev: Event): void => {
const toggleDiagnostics = (): void => {
const viewManager = this._api.getViewManager();
if (viewManager.getView()?.view === 'diagnostics') {
viewManager.setViewDefault();
} else {
viewManager.setViewByParameters({
params: { view: 'diagnostics' },
});
}
};
if (isAncestorInEventPath(this._element, ev, 'hui-dialog-edit-card')) {
toggleDiagnostics();
}
};
}