zombiebox
Version:
ZombieBox is a JavaScript framework for development of Smart TV and STB applications
598 lines (505 loc) • 12.3 kB
JavaScript
/*
* This file is part of the ZombieBox package.
*
* Copyright © 2012-2019, Interfaced
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import packageInfo from 'generated/package-info';
import {div, node, hide, show} from './html';
import {warn, setLogger, getLogger} from './console/console';
import BackButtonListener from './back-button-listener';
import IKeyHandler from './interfaces/i-key-handler';
import InputDispatcher from './input-dispatcher';
import LayerManager from './layer-manager';
import SceneOpener from './scene-opener';
import Console from './console/loggers/console';
import IDevice from './device/interfaces/i-device';
import {ResolutionInfo, ResolutionInfoItem} from './device/resolutions';
import Keys from './device/input/keys';
import EventPublisher from './events/event-publisher';
import HistoryManager from './history/history-manager';
import IHistoryManager from './history/interfaces/i-history-manager';
import Layer from './layers/layer';
import Scene from './layers/scene';
/**
* @abstract
* @implements {IKeyHandler}
*/
export default class AbstractApplication extends EventPublisher {
/**
*/
constructor() {
super();
/**
* @type {IDevice}
*/
this.device = null;
/**
* Application container where layers should be added
* @type {HTMLDivElement}
* @protected
*/
this._layerContainer = null;
/**
* Additional container to store device-dependent elements and objects
* @type {HTMLDivElement}
* @protected
*/
this._pluginContainer = null;
/**
* Additional container to store system level child layers
* @type {HTMLDivElement}
* @protected
*/
this._systemContainer = null;
/**
* @deprecated
* TODO: remove in 2.2.0
* Video container (emulation for html5 player to work as devices do)
* @type {HTMLDivElement}
* @protected
*/
this._videoContainer = null;
/**
* Application body
* @type {HTMLDivElement}
* @protected
*/
this._body = null;
/**
* @type {LayerManager}
* @protected
*/
this._layerManager = null;
/**
* @type {SceneOpener}
* @protected
*/
this._sceneOpener;
/**
* @type {IHistoryManager}
* @protected
*/
this._historyManager = null;
/**
*
* @type {InputDispatcher}
* @protected
*/
this._inputDispatcher = null;
/**
* System-level layer to show exception messages and other similar stuff
* @type {Layer}
* @protected
*/
this._systemLayer = null;
/**
* @type {boolean}
* @protected
*/
this._isSystemLayerShown = false;
/**
* DOM ready and created all application nodes.
* Fired with: nothing
* @const {string}
*/
this.EVENT_DOM_READY = 'dom-ready';
/**
* Device ready.
* Fired with: IDevice
* @const {string}
*/
this.EVENT_DEVICE_READY = 'device-ready';
/**
* Application ready to start.
* Fired with: {Object}
* @const {string}
*/
this.EVENT_READY = 'ready';
/**
* Application starting.
* Fired with: {Object} launch params
* @const {string}
*/
this.EVENT_START = 'start';
if (window.hasOwnProperty('console') && !getLogger()) {
setLogger(new Console());
}
this.on(this.EVENT_DOM_READY, () => this._loadDevice());
this.on(this.EVENT_DEVICE_READY, (eventName, device) => this._onDeviceReady(eventName, device));
this.on(this.EVENT_READY, () => this.onReady());
this.on(this.EVENT_START, (eventName, params) => this.onStart(params));
window.addEventListener('load', this._createAppDOM.bind(this), false);
}
/**
* @override
*/
processKey(zbKey, event) {
let result = false;
const currentLayer = this.getCurrentLayer();
if (currentLayer) {
// Current layer processes key
result = currentLayer.processKey(zbKey, event);
if (!result) {
result = this._processKey(zbKey, event);
}
} else if (!result) {
result = this._processKey(zbKey, event);
}
return result;
}
/**
* Clear navigation history and open home scene
* @abstract
* @return {?Promise}
*/
home() {}
/**
* Clear navigation history.
*/
clearHistory() {
this._historyManager.clear();
}
/**
* Exit from application.
*/
exit() {
const r = this.onExit();
if (r && r instanceof Promise) {
const exit = this.device.exit.bind(this.device);
r.then(exit, exit);
} else {
this.device.exit();
}
}
/**
* @return {Promise}
*/
back() {
if (!this._historyManager.canBack()) {
this._backOnEmptyHistory();
return Promise.resolve(null);
}
const blockId = this.device.input.block();
const unblock = this.device.input.unblock.bind(this.device.input, blockId);
return this._historyManager.back()
.then(unblock, unblock);
}
/**
* @return {Promise}
*/
forward() {
if (!this._historyManager.canForward()) {
return Promise.resolve(null);
}
const blockId = this.device.input.block();
const unblock = this.device.input.unblock(blockId);
return this._historyManager.forward()
.then(unblock, unblock);
}
/**
* @param {Scene} scene
* @param {string} name
*/
addScene(scene, name) {
this._layerManager.register(scene, name);
}
/**
* @return {?Layer}
*/
getCurrentLayer() {
if (this._isSystemLayerShown) {
return this._systemLayer;
}
return this._layerManager.getCurrentLayer();
}
/**
* @return {boolean}
*/
isSystemLayerShown() {
return this._isSystemLayerShown;
}
/**
* Create new instance of layerClassName and append it to the system container
* @param {Function} layerClassName
* @param {*=} params
* @return {Layer}
*/
showChildLayer(layerClassName, params) {
show(this._systemContainer);
return this._systemLayer.showChildLayer(layerClassName, params);
}
/**
* Show an existing instance of layer and append it to the system container
* @param {Layer} layer
*/
showChildLayerInstance(layer) {
show(this._systemContainer);
this._systemLayer.showChildLayerInstance(layer);
}
/**
* @param {Layer} layer
*/
closeChildLayer(layer) {
this._systemLayer.closeChildLayer(layer);
}
/**
* Set transparent background for layers container and show device video.
*/
showVideo() {
this._layerContainer.classList.add('_transparent');
}
/**
* Hide device video and restore layer container background.
*/
hideVideo() {
this._layerContainer.classList.remove('_transparent');
}
/**
* Toggle video state.
*/
toggleVideo() {
if (this.isVideoVisible()) {
this.hideVideo();
} else {
this.showVideo();
}
}
/**
* @return {boolean}
*/
isVideoVisible() {
return this._layerContainer.classList.contains('_transparent');
}
/**
* @deprecated
* TODO: remove in 2.2.0
* Return video container.
* @return {?HTMLElement}
*/
getVideoContainer() {
return this._videoContainer;
}
/**
* Return plugin container
* @return {?HTMLElement}
*/
getPluginContainer() {
return this._pluginContainer;
}
/**
* @return {InputDispatcher}
*/
getInputDispatcher() {
return this._inputDispatcher;
}
/**
* @return {SceneOpener}
*/
getSceneOpener() {
return this._sceneOpener;
}
/**
* @return {HTMLDivElement}
*/
getBody() {
return this._body;
}
/**
* @return {LayerManager}
*/
getLayerManager() {
return this._layerManager;
}
/**
* @return {HTMLDivElement}
*/
getLayerContainer() {
return this._layerContainer;
}
/**
* @return {Layer}
*/
getSystemLayer() {
return this._systemLayer;
}
/**
* @return {Promise|undefined}
*/
onExit() {
return undefined;
}
/**
* Called when document and device are ready
* @protected
*/
onReady() {/* For override */}
/**
* Method you use to launch home scene of an application
* @param {Object} launchParams
* @protected
*/
onStart(launchParams) { // eslint-disable-line no-unused-vars
/* For override */
}
/**
* @param {string} eventName
* @param {IDevice} device
* @protected
*/
_onDeviceReady(eventName, device) {
new BackButtonListener(this.processKey.bind(this, Keys.BACK));
this.device = device;
this.device.storage.setKeyPrefix(packageInfo['name']);
this._body.classList.add(this.device.info.type());
this._appendScreenSizeClass();
this._createInputDispatcher();
this._createSystemLayer();
this._setupLayerManager();
this._fireEvent(this.EVENT_READY);
this._fireEvent(this.EVENT_START, device.getLaunchParams());
}
/**
* @protected
*/
_onSystemLayerShown() {
show(this._systemContainer);
if (!this._isSystemLayerShown) {
const currentLayer = this._layerManager.getCurrentLayer();
if (currentLayer) {
currentLayer.blur();
}
}
this._isSystemLayerShown = true;
}
/**
* @protected
*/
_onSystemLayerHidden() {
if (!this._systemLayer.hasChildLayers()) {
const currentLayer = this._layerManager.getCurrentLayer();
if (currentLayer) {
currentLayer.focus();
}
hide(this._systemContainer);
this._isSystemLayerShown = false;
}
}
/**
* Load current device.
* @protected
*/
_loadDevice() {
const device = this._createDevice();
device.on(device.EVENT_READY, this._fireEvent.bind(this, this.EVENT_DEVICE_READY, device));
device.init();
}
/**
* @abstract
* @return {IDevice}
* @protected
*/
_createDevice() {}
/**
* @param {Keys} zbKey
* @param {(KeyboardEvent|WheelEvent)=} event
* @return {boolean} True if Key handled, false if not
* @protected
*/
_processKey(zbKey, event) {
let result = false;
switch (zbKey) {
// Back to previous scene/layer
case Keys.BACK:
this.back();
if (event) {
event.preventDefault();
}
result = true;
break;
}
// Warning for unhandled key
if (!result) {
warn(
'Unhandled zbKey ' + zbKey +
' (' + (event ? 'keyCode ' + event.keyCode : 'no keyboard event') + ')'
);
}
return result;
}
/**
* Detect max suitable screen size and set related CSS class name to body tag
* @protected
*/
_appendScreenSizeClass() {
const resolution = ResolutionInfo[this.device.info.osdResolutionType()];
const resolutionClassName = resolution ? ('zb-' + resolution.name) : 'zb-unknown-resolution';
this._appendViewportSize(resolution);
this._body.classList.add(resolutionClassName);
}
/**
* Add viewport and set its size
* @param {ResolutionInfoItem} resolution
* @protected
*/
_appendViewportSize(resolution) {
const meta = node('meta');
meta.name = 'viewport';
meta.content = 'width=' + resolution.width;
document.head.appendChild(meta);
}
/**
* Called on BACK button with empty history
* @protected
*/
_backOnEmptyHistory() {
this.exit();
}
/**
* Create DOM containers for app start.
* @protected
*/
_createAppDOM() {
// Create app DOM nodes
this._body = div('zb-body');
document.body.appendChild(this._body);
this._videoContainer = div('zb-video-container zb-fullscreen');
this._layerContainer = div('zb-layer-container zb-fullscreen');
this._systemContainer = div('zb-system-container zb-fullscreen');
this._pluginContainer = div('zb-plugin-container');
this._body.appendChild(this._layerContainer);
this._body.appendChild(this._systemContainer);
this._body.appendChild(this._pluginContainer);
hide(this._systemContainer);
this._fireEvent(this.EVENT_DOM_READY);
}
/**
* @protected
*/
_createInputDispatcher() {
this._inputDispatcher = new InputDispatcher(this);
this._inputDispatcher.setInput(this.device.input);
this._inputDispatcher.init();
}
/**
* @protected
*/
_createSystemLayer() {
this._systemLayer = new Layer();
this._systemLayer.on(this._systemLayer.EVENT_CHILD_LAYER_SHOWN, this._onSystemLayerShown.bind(this));
this._systemLayer.on(this._systemLayer.EVENT_CHILD_LAYER_HIDDEN, this._onSystemLayerHidden.bind(this));
this._systemContainer.appendChild(this._systemLayer.getRoot());
}
/**
* @protected
*/
_setupLayerManager() {
this._layerManager = new LayerManager(this._layerContainer);
this._historyManager = new HistoryManager();
this._sceneOpener = new SceneOpener();
this._sceneOpener.layerManager = this._layerManager;
this._sceneOpener.historyManager = this._historyManager;
}
}