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): 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(); } }; }