bitmovin-player-ui
Version:
Bitmovin Player UI Framework
159 lines (135 loc) • 5.48 kB
text/typescript
import { NavigationGroup } from './NavigationGroup';
import { RootNavigationGroup } from './RootNavigationGroup';
import { NodeEventSubscriber } from './NodeEventSubscriber';
import { SeekBarHandler } from './SeekBarHandler';
import { getKeyMapForPlatform } from './getKeyMapForPlatform';
import { Action, Direction, KeyMap } from './types';
import { isAction, isDirection } from './TypeGuards';
/**
* SpatialNavigation keeps track of all navigation groups, and updates the active navigation group when visibility
* changes on group container.
*
* It listens to key events, and triggers either handleNavigation or handleAction on the active group.
* SeekBarHandler will get instantiated with root navigation group and disposed on release of the spatial navigation.
*/
export class SpatialNavigation {
private unsubscribeVisibilityChangesFns: (() => void)[];
private readonly navigationGroups: NavigationGroup[] = [];
private readonly activeNavigationGroups: NavigationGroup[];
private readonly eventSubscriber: NodeEventSubscriber;
private readonly seekBarHandler: SeekBarHandler;
private readonly keyMap: KeyMap;
constructor(rootNavigationGroup: RootNavigationGroup, ...navigationGroups: NavigationGroup[]) {
this.seekBarHandler = new SeekBarHandler(rootNavigationGroup);
this.activeNavigationGroups = [];
this.unsubscribeVisibilityChangesFns = [];
this.eventSubscriber = new NodeEventSubscriber();
this.navigationGroups = [rootNavigationGroup, ...navigationGroups];
this.keyMap = getKeyMapForPlatform();
this.subscribeToNavigationGroupVisibilityChanges();
this.attachKeyEventHandler();
this.enableDefaultNavigationGroup();
}
private attachKeyEventHandler(): void {
this.eventSubscriber.on(document, 'keydown', this.handleKeyEvent, true);
}
private onShow = (group: NavigationGroup): void => {
this.activeNavigationGroups.push(group);
this.updateEnabledNavigationGroup();
};
private onHide = (group: NavigationGroup): void => {
const groupIndex = this.activeNavigationGroups.findIndex(other => other === group);
if (groupIndex > -1) {
group.disable();
this.activeNavigationGroups.splice(groupIndex, 1);
this.updateEnabledNavigationGroup();
}
};
/**
* Subscribes to onHide and onShow on all navigation groups containers as Spatial navigation tracks active navigation
* group based on their container visibility.
*/
private subscribeToNavigationGroupVisibilityChanges(): void {
this.navigationGroups.forEach(group => {
const onShowHandler = () => this.onShow(group);
const onHideHandler = () => this.onHide(group);
group.container.onShow.subscribe(onShowHandler);
group.container.onHide.subscribe(onHideHandler);
this.unsubscribeVisibilityChangesFns.push(
() => group.container.onShow.unsubscribe(onShowHandler),
() => group.container.onHide.unsubscribe(onHideHandler),
);
});
}
private unsubscribeFromNavigationGroupVisibilityChanges(): void {
this.unsubscribeVisibilityChangesFns.forEach(unsub => unsub());
this.unsubscribeVisibilityChangesFns = [];
}
/**
* It will enable group of which container is currently shown
* If there are no groups with containers that are currently visible, it will enable root navigation group
*/
private enableDefaultNavigationGroup(): void {
const isShown = (group: NavigationGroup) => group.container.isShown();
const groupToEnable = this.navigationGroups.find(isShown) ?? this.navigationGroups[0];
if (groupToEnable) {
this.activeNavigationGroups.push(groupToEnable);
this.updateEnabledNavigationGroup();
}
}
/**
* Disables navigation groups that are no longer active and calls enable on last pushed navigation group
*/
private updateEnabledNavigationGroup(): void {
this.activeNavigationGroups.forEach((group, idx) => {
if (idx < this.activeNavigationGroups.length - 1) {
group.disable();
} else {
group.enable();
}
});
}
/**
* Returns currently active navigation group
*/
public getActiveNavigationGroup(): NavigationGroup | undefined {
return this.activeNavigationGroups[this.activeNavigationGroups.length - 1];
}
/**
* Checks if keyboard event keycode is tracked either as Direction or Action and calls appropriate handler on active
* navigation group
*
* @param e {KeyboardEvent}
*/
private handleKeyEvent = (e: KeyboardEvent): void => {
const event: Direction | Action | undefined = this.keyMap[getKeyCode(e)];
const active = this.getActiveNavigationGroup();
if (!active || !active.container || active.container.isHidden() || active.container.isDisabled()) {
return;
}
if (isDirection(event)) {
active.handleNavigation(event);
e.preventDefault();
e.stopPropagation();
}
if (isAction(event)) {
active.handleAction(event);
e.preventDefault();
e.stopPropagation();
}
};
/**
* Dispose of SpatialNavigation
*
* Remove all event handlers, release seekbar handler and release all navigation groups.
*/
public release(): void {
this.unsubscribeFromNavigationGroupVisibilityChanges();
this.eventSubscriber.release();
this.navigationGroups.forEach(group => group.release());
this.seekBarHandler.release();
}
}
function getKeyCode(event: KeyboardEvent): number {
return event.keyCode;
}