Initial support for grid for live and media viewer.
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
FrigateCardCustomAction,
|
||||
frigateCardCustomActionSchema,
|
||||
FrigateCardViewAction,
|
||||
ViewDisplayMode,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,7 @@ export function createFrigateCardCustomAction(
|
||||
camera?: string;
|
||||
media_player?: string;
|
||||
media_player_action?: 'play' | 'stop';
|
||||
display_mode?: ViewDisplayMode;
|
||||
},
|
||||
): FrigateCardCustomAction | null {
|
||||
if (action === 'camera_select' || action === 'live_substream_select') {
|
||||
@@ -67,6 +69,17 @@ export function createFrigateCardCustomAction(
|
||||
...(args.cardID && { card_id: args.cardID }),
|
||||
};
|
||||
}
|
||||
if (action === 'display_mode_select') {
|
||||
if (!args?.display_mode) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: action,
|
||||
display_mode: args?.display_mode,
|
||||
...(args.cardID && { card_id: args.cardID }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: action,
|
||||
|
||||
@@ -221,3 +221,8 @@ export const setOrRemoveAttribute = (
|
||||
element.removeAttribute(name);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow typescript to narrow types based on truthy filter.
|
||||
*/
|
||||
export const filterTruthy = <T>(x: T | false | undefined | null | '' | 0): x is T => !!x;
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import Masonry from 'masonry-layout';
|
||||
import { MediaLoadedInfo, ViewDisplayConfig } from '../types';
|
||||
import { dispatchFrigateCardEvent, setOrRemoveAttribute } from './basic';
|
||||
import {
|
||||
FrigateMediaLoadedEventTarget,
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaUnloadedEvent,
|
||||
} from './media-info';
|
||||
|
||||
// The default minimum cell width: if the columns are not specified this value
|
||||
// is used to compute the number of columns, always trying to keep each cell as
|
||||
// at least this width. On Android, a card in portrait mode is 396 pixels, and
|
||||
// we'd like to support two cells wide in that configuration.
|
||||
const MEDIA_GRID_DEFAULT_MIN_CELL_WIDTH = 190;
|
||||
const MEDIA_GRID_DEFAULT_IDEAL_CELL_WIDTH = 600;
|
||||
const MEDIA_GRID_DEFAULT_SELECTED_WIDTH_FACTOR = 2.0;
|
||||
|
||||
type GridID = string;
|
||||
type MediaGridChild = HTMLElement & FrigateMediaLoadedEventTarget;
|
||||
type MediaGridContents = Map<GridID, MediaGridChild>;
|
||||
|
||||
export interface MediaGridSelected {
|
||||
selected: GridID;
|
||||
}
|
||||
|
||||
export interface MediaGridConstructorOptions {
|
||||
selected?: GridID;
|
||||
idAttribute?: string;
|
||||
}
|
||||
|
||||
const SELECT_CHILD_EVENTS = ['click', 'touchend'];
|
||||
|
||||
export class MediaGridController {
|
||||
protected _host: HTMLElement;
|
||||
|
||||
protected _selected: GridID | null;
|
||||
protected _mediaLoadedInfoMap: Map<GridID, MediaLoadedInfo> = new Map();
|
||||
protected _gridContents: MediaGridContents = new Map();
|
||||
protected _masonry: Masonry | null = null;
|
||||
protected _displayConfig: ViewDisplayConfig | null = null;
|
||||
protected _hostWidth: number;
|
||||
protected _idAttribute: string;
|
||||
|
||||
protected _throttledLayout = throttle(
|
||||
() => this._masonry?.layout?.(),
|
||||
// Throttle layout calls to larger than the masonry.js transitionDuration
|
||||
// value specified below.
|
||||
500,
|
||||
{ trailing: true, leading: false },
|
||||
);
|
||||
|
||||
protected _mutationObserver = new MutationObserver(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(_mutations: MutationRecord[], _observer: MutationObserver) =>
|
||||
this._calculateGridContentsFromHost(),
|
||||
);
|
||||
protected _cellResizeObserver = new ResizeObserver(this._cellResizeHandler.bind(this));
|
||||
protected _hostResizeObserver = new ResizeObserver(this._hostResizeHandler.bind(this));
|
||||
|
||||
constructor(host: HTMLElement, options?: MediaGridConstructorOptions) {
|
||||
this._host = host;
|
||||
this._selected = options?.selected ?? null;
|
||||
this._idAttribute = options?.idAttribute ?? 'grid-id';
|
||||
this._hostWidth = this._host.getBoundingClientRect().width;
|
||||
this._hostResizeObserver.observe(host);
|
||||
|
||||
this._calculateGridContentsFromHost();
|
||||
this._mutationObserver.observe(host, { childList: true });
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this._hostResizeObserver.disconnect();
|
||||
this._cellResizeObserver.disconnect();
|
||||
this._mediaLoadedInfoMap.clear();
|
||||
this._masonry?.destroy?.();
|
||||
this._masonry = null;
|
||||
|
||||
for (const child of this._gridContents.values()) {
|
||||
this._removeChildEventListeners(child);
|
||||
}
|
||||
this._gridContents.clear();
|
||||
}
|
||||
|
||||
public setDisplayConfig(displayConfig: ViewDisplayConfig | null): void {
|
||||
this._displayConfig = displayConfig;
|
||||
this._calculateGridContentsFromHost();
|
||||
}
|
||||
|
||||
public getGridContents(): MediaGridContents {
|
||||
return this._gridContents;
|
||||
}
|
||||
|
||||
public getGridSize(): number {
|
||||
return this._gridContents.size;
|
||||
}
|
||||
|
||||
public getSelected(): GridID | null {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
public selectCell(id: GridID) {
|
||||
if (this._selected === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selected = id;
|
||||
dispatchFrigateCardEvent(this._host, 'media-grid:selected', { selected: id });
|
||||
|
||||
const mediaLoadedInfo = this._mediaLoadedInfoMap.get(id);
|
||||
if (mediaLoadedInfo) {
|
||||
dispatchExistingMediaLoadedInfoAsEvent(this._host, mediaLoadedInfo);
|
||||
}
|
||||
|
||||
this._updateSelectedStylesOnElements();
|
||||
|
||||
// Sizes may change when an element is selected, so re-do the layout (must
|
||||
// come after the call to _updateStylesOnElements in order to ensure the
|
||||
// right styles are applied first).
|
||||
this._throttledLayout();
|
||||
}
|
||||
|
||||
public unselectAll() {
|
||||
if (this._selected !== null) {
|
||||
dispatchMediaUnloadedEvent(this._host);
|
||||
dispatchFrigateCardEvent(this._host, 'media-grid:unselected');
|
||||
}
|
||||
this._selected = null;
|
||||
this._updateSelectedStylesOnElements();
|
||||
}
|
||||
|
||||
protected _calculateGridContentsFromHost(): void {
|
||||
let childrenElements: Element[];
|
||||
|
||||
if (this._host instanceof HTMLSlotElement) {
|
||||
childrenElements = this._host.assignedElements({ flatten: true });
|
||||
} else {
|
||||
childrenElements = [...this._host.children];
|
||||
}
|
||||
|
||||
const gridContents: MediaGridContents = new Map();
|
||||
for (const child of childrenElements) {
|
||||
if (child instanceof HTMLElement) {
|
||||
const id = child.getAttribute(this._idAttribute) || String(gridContents.size);
|
||||
gridContents.set(id, child);
|
||||
}
|
||||
}
|
||||
|
||||
this._setGridContents(gridContents);
|
||||
}
|
||||
|
||||
protected _setGridContents(elements: MediaGridContents): void {
|
||||
this._gridContents = elements;
|
||||
|
||||
// Remove media loaded info objects that belong to objects no longer in the
|
||||
// grid.
|
||||
for (const key of this._mediaLoadedInfoMap.keys()) {
|
||||
if (!elements.has(key)) {
|
||||
this._mediaLoadedInfoMap.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._selected !== null && !this._gridContents.has(this._selected)) {
|
||||
this.unselectAll();
|
||||
}
|
||||
|
||||
for (const element of elements.values()) {
|
||||
this._removeChildEventListeners(element);
|
||||
this._addChildEventListeners(element);
|
||||
}
|
||||
|
||||
this._setColumnSizeStyles();
|
||||
this._createMasonry();
|
||||
|
||||
// Observe grid elements for size changes.
|
||||
this._cellResizeObserver.disconnect();
|
||||
for (const child of elements.values()) {
|
||||
this._cellResizeObserver.observe(child);
|
||||
}
|
||||
|
||||
this._updateSelectedStylesOnElements();
|
||||
this._setColumnSizeStyles();
|
||||
}
|
||||
|
||||
protected _handleMediaLoadedInfoEvent = (ev: CustomEvent<MediaLoadedInfo>): void => {
|
||||
const eventPath = ev.composedPath();
|
||||
|
||||
for (const [id, element] of this._gridContents.entries()) {
|
||||
if (eventPath.includes(element)) {
|
||||
this._mediaLoadedInfoMap.set(id, ev.detail);
|
||||
if (id !== this._selected) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected _hostResizeHandler(): void {
|
||||
const dimensions = this._host.getBoundingClientRect();
|
||||
|
||||
// Only resize things if the width has changed. It is expected that the
|
||||
// height may change during the layout.
|
||||
if (dimensions.width !== this._hostWidth) {
|
||||
this._hostWidth = dimensions.width;
|
||||
|
||||
// Reset the column CSS sizes first.
|
||||
this._setColumnSizeStyles();
|
||||
|
||||
// Need to recreate the masonry layout since the column width will differ.
|
||||
this._createMasonry();
|
||||
}
|
||||
}
|
||||
|
||||
protected _cellResizeHandler(): void {
|
||||
this._throttledLayout();
|
||||
}
|
||||
|
||||
protected _removeChildEventListeners(child: MediaGridChild): void {
|
||||
for (const event of SELECT_CHILD_EVENTS) {
|
||||
child.removeEventListener(event, this._handleSelectGridCellEvent, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
child.removeEventListener(
|
||||
'frigate-card:media:loaded',
|
||||
this._handleMediaLoadedInfoEvent,
|
||||
);
|
||||
}
|
||||
|
||||
protected _addChildEventListeners(child: MediaGridChild): void {
|
||||
for (const event of SELECT_CHILD_EVENTS) {
|
||||
child.addEventListener(event, this._handleSelectGridCellEvent, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
child.addEventListener(
|
||||
'frigate-card:media:loaded',
|
||||
this._handleMediaLoadedInfoEvent,
|
||||
);
|
||||
}
|
||||
|
||||
protected _createMasonry(): void {
|
||||
if (this._masonry) {
|
||||
this._masonry.destroy?.();
|
||||
}
|
||||
|
||||
this._masonry = new Masonry(this._host, {
|
||||
columnWidth: this._getColumnSize(),
|
||||
initLayout: false,
|
||||
percentPosition: true,
|
||||
transitionDuration: '0.3s',
|
||||
});
|
||||
this._masonry.addItems?.([...this._gridContents.values()]);
|
||||
this._throttledLayout();
|
||||
}
|
||||
|
||||
protected _handleSelectGridCellEvent = (ev: Event): void => {
|
||||
const eventPath = ev.composedPath();
|
||||
|
||||
for (const [id, element] of this._gridContents.entries()) {
|
||||
if (eventPath.includes(element)) {
|
||||
if (this._selected !== id) {
|
||||
this.selectCell(id);
|
||||
ev.stopPropagation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected _updateSelectedStylesOnElements(): void {
|
||||
for (const [id, element] of this._gridContents.entries()) {
|
||||
setOrRemoveAttribute(element, id === this._selected, 'selected');
|
||||
|
||||
// Explicitly use an 'unselected' attribute vs a :not(selected) such that
|
||||
// a carousel with neither selected nor unselected will behave normally.
|
||||
// This matches a css selector in viewer-carousel.scss .
|
||||
setOrRemoveAttribute(element, id !== this._selected, 'unselected');
|
||||
}
|
||||
}
|
||||
|
||||
protected _getColumnSize(): number {
|
||||
return Math.round(this._hostWidth / this._getColumns());
|
||||
}
|
||||
|
||||
protected _getColumns(): number {
|
||||
if (this._displayConfig?.grid_columns) {
|
||||
return this._displayConfig?.grid_columns;
|
||||
}
|
||||
|
||||
const maxColumns = this._displayConfig?.grid_max_columns ?? Infinity;
|
||||
|
||||
// See if we can get a multi-column layout using the ideal cell width.
|
||||
const idealColumns = Math.min(
|
||||
maxColumns,
|
||||
Math.floor(this._hostWidth / MEDIA_GRID_DEFAULT_IDEAL_CELL_WIDTH),
|
||||
);
|
||||
if (idealColumns > 1) {
|
||||
return idealColumns;
|
||||
}
|
||||
|
||||
// If not, get a multi-column view using the minimum cell width.
|
||||
const minColumns = Math.floor(
|
||||
Math.min(maxColumns, this._hostWidth / MEDIA_GRID_DEFAULT_MIN_CELL_WIDTH),
|
||||
);
|
||||
|
||||
// Last result use at least 1 column.
|
||||
return Math.max(1, minColumns);
|
||||
}
|
||||
|
||||
protected _setColumnSizeStyles(): void {
|
||||
this._host.style.setProperty(
|
||||
'--frigate-card-grid-column-size',
|
||||
`${this._getColumnSize()}px`,
|
||||
);
|
||||
|
||||
this._host.style.setProperty(
|
||||
'--frigate-card-grid-selected-width-factor',
|
||||
`${
|
||||
this._displayConfig?.grid_selected_width_factor ??
|
||||
MEDIA_GRID_DEFAULT_SELECTED_WIDTH_FACTOR
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -111,3 +111,33 @@ export function isValidMediaLoadedInfo(info: MediaLoadedInfo): boolean {
|
||||
info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF
|
||||
);
|
||||
}
|
||||
|
||||
// Facilities correct Typescript typing of media:loaded event handlers.
|
||||
export interface FrigateMediaLoadedEventTarget extends EventTarget {
|
||||
addEventListener(
|
||||
event: 'frigate-card:media:loaded',
|
||||
listener: (
|
||||
this: FrigateMediaLoadedEventTarget,
|
||||
ev: CustomEvent<MediaLoadedInfo>,
|
||||
) => void,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
removeEventListener(
|
||||
event: 'frigate-card:media:loaded',
|
||||
listener: (
|
||||
this: FrigateMediaLoadedEventTarget,
|
||||
ev: CustomEvent<MediaLoadedInfo>,
|
||||
) => void,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
removeEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
}
|
||||
|
||||
+28
-29
@@ -1,20 +1,20 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { ViewContext } from 'view';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { MediaQuery } from '../camera-manager/types';
|
||||
import { dispatchFrigateCardErrorEvent } from '../components/message';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
|
||||
import { CardWideConfig, ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
|
||||
import { View } from '../view/view';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import {
|
||||
EventMediaQueries,
|
||||
MediaQueries,
|
||||
RecordingMediaQueries,
|
||||
} from '../view/media-queries';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { getAllDependentCameras } from './camera.js';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { dispatchFrigateCardErrorEvent } from '../components/message';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { View } from '../view/view';
|
||||
import { errorToConsole } from './basic';
|
||||
import { MediaQuery } from '../camera-manager/types';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
|
||||
import { getAllDependentCameras } from './camera.js';
|
||||
|
||||
type ResultSelectType = 'latest' | 'time' | 'none';
|
||||
|
||||
@@ -25,13 +25,16 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
||||
cardWideConfig: CardWideConfig,
|
||||
view: View,
|
||||
options?: {
|
||||
allCameras?: boolean;
|
||||
mediaType?: ClipsOrSnapshotsOrAll;
|
||||
targetView?: FrigateCardView;
|
||||
select?: ResultSelectType;
|
||||
},
|
||||
): Promise<void> => {
|
||||
const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
|
||||
if (!cameraIDs) {
|
||||
const cameraIDs = options?.allCameras
|
||||
? cameraManager.getStore().getVisibleCameraIDs()
|
||||
: getAllDependentCameras(cameraManager, view.camera);
|
||||
if (!cameraIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,12 +87,15 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
||||
cardWideConfig: CardWideConfig,
|
||||
view: View,
|
||||
options?: {
|
||||
allCameras?: boolean;
|
||||
targetView?: 'recording' | 'recordings';
|
||||
select?: ResultSelectType;
|
||||
},
|
||||
): Promise<void> => {
|
||||
const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
|
||||
if (!cameraIDs) {
|
||||
const cameraIDs = options?.allCameras
|
||||
? cameraManager.getStore().getVisibleCameraIDs()
|
||||
: getAllDependentCameras(cameraManager, view.camera);
|
||||
if (!cameraIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,12 +167,7 @@ export const executeMediaQueryForView = async (
|
||||
return null;
|
||||
}
|
||||
|
||||
const queryResults = new MediaQueriesResults(
|
||||
mediaArray,
|
||||
options?.select === 'latest' && mediaArray.length
|
||||
? mediaArray.length - 1
|
||||
: undefined,
|
||||
);
|
||||
const queryResults = new MediaQueriesResults({ results: mediaArray });
|
||||
let viewerContext: ViewContext | undefined = {};
|
||||
|
||||
if (options?.select === 'time' && options?.targetTime) {
|
||||
@@ -180,16 +181,14 @@ export const executeMediaQueryForView = async (
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
view
|
||||
?.evolve({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
view: options?.targetView,
|
||||
camera: options?.targetCameraID,
|
||||
})
|
||||
.mergeInContext(viewerContext) ?? null
|
||||
);
|
||||
return view
|
||||
.evolve({
|
||||
query: query,
|
||||
queryResults: queryResults,
|
||||
view: options?.targetView,
|
||||
camera: options?.targetCameraID,
|
||||
})
|
||||
.mergeInContext(viewerContext);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -201,7 +200,7 @@ export const executeMediaQueryForView = async (
|
||||
*/
|
||||
export const findBestMediaIndex = (
|
||||
mediaArray: ViewMedia[],
|
||||
targetTime: Date
|
||||
targetTime: Date,
|
||||
): number | null => {
|
||||
let bestMatch:
|
||||
| {
|
||||
|
||||
@@ -5,11 +5,11 @@ import { CameraManager } from '../camera-manager/manager';
|
||||
import { FRIGATE_BUTTON_MENU_ICON } from '../const';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import {
|
||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||
FrigateCardConfig,
|
||||
FrigateCardCustomAction,
|
||||
MediaLoadedInfo,
|
||||
MenuButton,
|
||||
FrigateCardConfig,
|
||||
FrigateCardCustomAction,
|
||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||
MediaLoadedInfo,
|
||||
MenuButton,
|
||||
} from '../types';
|
||||
import { View } from '../view/view';
|
||||
import { createFrigateCardCustomAction } from './action';
|
||||
@@ -388,10 +388,28 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.screenshot,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.screenshot'),
|
||||
tap_action: createFrigateCardCustomAction('screenshot') as FrigateCardCustomAction,
|
||||
tap_action: createFrigateCardCustomAction(
|
||||
'screenshot',
|
||||
) as FrigateCardCustomAction,
|
||||
});
|
||||
}
|
||||
|
||||
if (view.hasMultipleDisplayModes(visibleCameras.size)) {
|
||||
const isGrid = view.isGrid();
|
||||
const action = createFrigateCardCustomAction('display_mode_select', {
|
||||
display_mode: isGrid ? 'single' : 'grid',
|
||||
});
|
||||
if (action) {
|
||||
buttons.push({
|
||||
icon: isGrid ? 'mdi:grid-off' : 'mdi:grid',
|
||||
...config.menu.buttons.display_mode,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.display_mode'),
|
||||
tap_action: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
||||
style: this._getStyleFromActions(config, view, button),
|
||||
...button,
|
||||
|
||||
+10
-5
@@ -141,15 +141,20 @@ export class Zoom {
|
||||
}
|
||||
|
||||
public deactivate(): void {
|
||||
const unregisterListener = (events: string[], func: (ev: Event) => void) => {
|
||||
const unregisterListener = (
|
||||
events: string[],
|
||||
func: (ev: Event) => void,
|
||||
options?: EventListenerOptions,
|
||||
) => {
|
||||
events.forEach((eventName) => {
|
||||
this._element.removeEventListener(eventName, func);
|
||||
this._element.removeEventListener(eventName, func, options);
|
||||
});
|
||||
};
|
||||
|
||||
unregisterListener(this._events['down'], this._downHandler);
|
||||
unregisterListener(this._events['move'], this._moveHandler);
|
||||
unregisterListener(this._events['up'], this._upHandler);
|
||||
unregisterListener(this._events['down'], this._downHandler, { capture: true });
|
||||
unregisterListener(this._events['move'], this._moveHandler, { capture: true });
|
||||
unregisterListener(this._events['up'], this._upHandler, { capture: true });
|
||||
unregisterListener(['wheel'], this._wheelHandler);
|
||||
unregisterListener(['click'], this._clickHandler, { capture: true });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user