bitmovin-player-ui
Version:
Bitmovin Player UI Framework
487 lines (486 loc) • 21.5 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SettingsPanel = exports.NavigationDirection = void 0;
var Container_1 = require("../Container");
var SelectBox_1 = require("./SelectBox");
var Timeout_1 = require("../../utils/Timeout");
var EventDispatcher_1 = require("../../EventDispatcher");
var SettingsPanelPage_1 = require("./SettingsPanelPage");
var getKeyMapForPlatform_1 = require("../../spatialnavigation/getKeyMapForPlatform");
var types_1 = require("../../spatialnavigation/types");
var NavigationDirection;
(function (NavigationDirection) {
NavigationDirection[NavigationDirection["Forwards"] = 0] = "Forwards";
NavigationDirection[NavigationDirection["Backwards"] = 1] = "Backwards";
})(NavigationDirection || (exports.NavigationDirection = NavigationDirection = {}));
/**
* A panel containing a list of {@link SettingsPanelPage items}.
*
* To configure pages just pass them in the components array.
*
* Example:
* let settingsPanel = new SettingsPanel({
* hidden: true,
* });
*
* let settingsPanelPage = new SettingsPanelPage({
* components: […]
* });
*
* let secondSettingsPanelPage = new SettingsPanelPage({
* components: […]
* });
*
* settingsPanel.addComponent(settingsPanelPage);
* settingsPanel.addComponent(secondSettingsPanelPage);
*
* For an example how to navigate between pages @see SettingsPanelPageNavigatorButton
*
* @category Components
*/
var SettingsPanel = /** @class */ (function (_super) {
__extends(SettingsPanel, _super);
function SettingsPanel(config) {
var _this = _super.call(this, config) || this;
_this.navigationStack = [];
_this.currentState = null;
_this.resetStateTimerId = null;
_this.shouldResetStateImmediately = false;
_this.settingsPanelEvents = {
onSettingsStateChanged: new EventDispatcher_1.EventDispatcher(),
onActivePageChanged: new EventDispatcher_1.EventDispatcher(),
};
_this.config = _this.mergeConfig(config, {
cssClass: 'ui-settings-panel',
hideDelay: 5000,
pageTransitionAnimation: true,
stateResetDelay: 5000,
}, _this.config);
_this.activePage = _this.getRootPage();
_this.onActivePageChangedEvent();
return _this;
}
SettingsPanel.prototype.configure = function (player, uimanager) {
var _this = this;
_super.prototype.configure.call(this, player, uimanager);
var config = this.getConfig();
uimanager.onControlsHide.subscribe(function () { return _this.hideHoveredSelectBoxes(); });
uimanager.onComponentViewModeChanged.subscribe(function (_, _a) {
var mode = _a.mode;
return _this.trackComponentViewMode(mode);
});
if (config.hideDelay > -1) {
this.hideTimeout = new Timeout_1.Timeout(config.hideDelay, function () {
_this.hide();
_this.hideHoveredSelectBoxes();
});
this.getDomElement().on('mouseenter mousemove', function () {
_this.hideTimeout.reset();
});
this.getDomElement().on('mouseleave', function () {
// On mouse leave activate the timeout
_this.hideTimeout.reset();
});
this.getDomElement().on('focusin', function () {
_this.hideTimeout.clear();
});
this.getDomElement().on('focusout', function () {
_this.hideTimeout.reset();
});
}
if (config.pageTransitionAnimation) {
var handleResize = function () {
// Reset the dimension of the settingsPanel to let the browser calculate the new dimension after resizing
_this.getDomElement().css({ width: '', height: '' });
};
player.on(player.exports.PlayerEvent.PlayerResized, handleResize);
}
var maybeCloseSettingsPanel = function (event) {
var action = (0, getKeyMapForPlatform_1.getKeyMapForPlatform)()[event.keyCode];
if (action === types_1.Action.BACK) {
_this.hide();
_this.resetState();
}
};
var scheduleResetState = function () {
if (_this.resetStateTimerId !== null) {
clearTimeout(_this.resetStateTimerId);
_this.resetStateTimerId = null;
}
if (config.stateResetDelay > -1) {
_this.resetStateTimerId = window.setTimeout(function () { return _this.resetState(); }, config.stateResetDelay);
}
};
this.onHide.subscribe(function () {
if (_this.shouldResetStateImmediately) {
_this.currentState = null;
_this.shouldResetStateImmediately = false;
}
else {
_this.currentState = _this.maybeSaveCurrentState();
scheduleResetState();
}
if (config.hideDelay > -1) {
// Clear timeout when hidden from outside
_this.hideTimeout.clear();
}
// Since we don't reset the actual navigation here we need to simulate a onInactive event in case some panel
// needs to do something when they become invisible / inactive.
_this.activePage.onInactiveEvent();
document.removeEventListener('keyup', maybeCloseSettingsPanel);
});
this.onShow.subscribe(function () {
if (_this.resetStateTimerId !== null) {
clearTimeout(_this.resetStateTimerId);
_this.resetStateTimerId = null;
}
if (_this.currentState !== null) {
_this.restoreNavigationState(_this.currentState);
}
else {
// No saved state (was reset), ensure visual classes are updated
_this.updateActivePageClass();
}
// Since we don't need to navigate to the root page again we need to fire the onActive event when the settings
// panel gets visible.
_this.activePage.onActiveEvent();
if (config.hideDelay > -1) {
// Activate timeout when shown
_this.hideTimeout.start();
}
document.addEventListener('keyup', maybeCloseSettingsPanel);
});
// pass event from root page through
this.getRootPage().onSettingsStateChanged.subscribe(function () {
_this.onSettingsStateChangedEvent();
});
uimanager.onControlsHide.subscribe(function () {
_this.hide();
});
uimanager.onControlsShow.subscribe(function () {
if (_this.currentState !== null) {
_this.show();
}
});
this.updateActivePageClass();
};
/**
* Returns the current active / visible page
* @return {SettingsPanelPage}
*/
SettingsPanel.prototype.getActivePage = function () {
return this.activePage;
};
/**
* Sets the
* @deprecated Use {@link setActivePage} instead
* @param index
*/
SettingsPanel.prototype.setActivePageIndex = function (index) {
this.setActivePage(this.getPages()[index]);
};
/**
* Adds the passed page to the navigation stack and makes it visible.
* Use {@link popSettingsPanelPage} to navigate backwards.
*
* Results in no-op if the target page is the current page.
* @param targetPage
*/
SettingsPanel.prototype.setActivePage = function (targetPage) {
if (targetPage === this.getActivePage()) {
console.warn('Page is already the current one ... skipping navigation');
return;
}
this.navigateToPage(targetPage, this.getActivePage(), NavigationDirection.Forwards, !this.config.pageTransitionAnimation);
};
/**
* Resets the navigation stack by navigating back to the root page and displaying it.
*/
SettingsPanel.prototype.popToRootSettingsPanelPage = function () {
this.resetNavigation(this.config.pageTransitionAnimation);
};
/**
* Removes the current page from the navigation stack and makes the previous one visible.
* Results in a no-op if we are already on the root page.
*/
SettingsPanel.prototype.popSettingsPanelPage = function () {
if (this.navigationStack.length === 0) {
console.warn('Already on the root page ... skipping navigation');
return;
}
var targetPage = this.navigationStack[this.navigationStack.length - 2];
// The root part isn't part of the navigation stack so handle it explicitly here
if (!targetPage) {
targetPage = this.getRootPage();
}
var currentActivePage = this.activePage;
this.navigateToPage(targetPage, this.activePage, NavigationDirection.Backwards, !this.config.pageTransitionAnimation);
if (currentActivePage.getConfig().removeOnPop) {
this.removeComponent(currentActivePage);
this.updateComponents();
}
};
/**
* Checks if there are active settings within the root page of the settings panel.
* An active setting is a setting that is visible and enabled, which the user can interact with.
* @returns {boolean} true if there are active settings, false if the panel is functionally empty to a user
*/
SettingsPanel.prototype.rootPageHasActiveSettings = function () {
return this.getRootPage().hasActiveSettings();
};
/**
* Return all configured pages
* @returns {SettingsPanelPage[]}
*/
SettingsPanel.prototype.getPages = function () {
return this.config.components.filter(function (component) { return component instanceof SettingsPanelPage_1.SettingsPanelPage; });
};
/**
* Returns the root page of the settings panel.
* @returns {SettingsPanelPage}
*/
SettingsPanel.prototype.getRootPage = function () {
return this.getPages()[0];
};
Object.defineProperty(SettingsPanel.prototype, "onSettingsStateChanged", {
get: function () {
return this.settingsPanelEvents.onSettingsStateChanged.getEvent();
},
enumerable: false,
configurable: true
});
Object.defineProperty(SettingsPanel.prototype, "onActivePageChanged", {
get: function () {
return this.settingsPanelEvents.onActivePageChanged.getEvent();
},
enumerable: false,
configurable: true
});
SettingsPanel.prototype.hideAndReset = function () {
this.shouldResetStateImmediately = true;
this.hide();
this.resetState();
};
SettingsPanel.prototype.release = function () {
_super.prototype.release.call(this);
if (this.hideTimeout) {
this.hideTimeout.clear();
}
};
// Support adding settingsPanelPages after initialization
SettingsPanel.prototype.addComponent = function (component) {
if (this.getPages().length === 0 && component instanceof SettingsPanelPage_1.SettingsPanelPage) {
this.activePage = component;
this.onActivePageChangedEvent();
}
_super.prototype.addComponent.call(this, component);
};
SettingsPanel.prototype.addPage = function (page) {
this.addComponent(page);
this.updateComponents();
};
SettingsPanel.prototype.suspendHideTimeout = function () {
this.hideTimeout.suspend();
};
SettingsPanel.prototype.resumeHideTimeout = function () {
this.hideTimeout.resume(true);
};
SettingsPanel.prototype.updateActivePageClass = function () {
var _this = this;
this.getPages().forEach(function (page) {
if (page === _this.activePage) {
page.getDomElement().addClass(_this.prefixCss(SettingsPanel.CLASS_ACTIVE_PAGE));
}
else {
page.getDomElement().removeClass(_this.prefixCss(SettingsPanel.CLASS_ACTIVE_PAGE));
}
});
};
SettingsPanel.prototype.resetNavigation = function (resetNavigationOnShow) {
var sourcePage = this.getActivePage();
var rootPage = this.getRootPage();
if (sourcePage) {
// Since the onInactiveEvent was already fired in the onHide we need to suppress it here
if (!resetNavigationOnShow) {
sourcePage.onInactiveEvent();
}
}
this.navigationStack = [];
this.animateNavigation(rootPage, sourcePage, resetNavigationOnShow);
this.activePage = rootPage;
this.updateActivePageClass();
this.onActivePageChangedEvent();
};
Object.defineProperty(SettingsPanel.prototype, "wrapperScrollTop", {
get: function () {
var _a, _b;
return (_b = (_a = this.innerContainerElement.get(0)) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0;
},
set: function (value) {
var element = this.innerContainerElement.get(0);
if (element) {
element.scrollTop = value;
}
},
enumerable: false,
configurable: true
});
SettingsPanel.prototype.resetState = function () {
this.activePage = this.getRootPage();
this.navigationStack = [];
this.currentState = null;
this.resetStateTimerId = null;
if (this.isHidden()) {
// Clear dimensions only when hidden to avoid visible transition animation
this.getDomElement().css({ width: '', height: '' });
}
};
SettingsPanel.prototype.buildCurrentState = function () {
var pages = this.getPages();
var activePageIndex = pages.indexOf(this.getActivePage());
var navigationStackIndices = this.navigationStack.map(function (p) { return pages.indexOf(p); });
var panelElement = this.getDomElement().get(0);
return {
activePageIndex: activePageIndex,
navigationStackIndices: navigationStackIndices,
scrollTop: panelElement.scrollTop,
wrapperScrollTop: this.wrapperScrollTop,
};
};
SettingsPanel.prototype.isDefaultPanelState = function () {
var _a;
var panelElement = this.getDomElement().get(0);
var atRoot = this.getActivePage() === this.getRootPage();
var noNav = this.navigationStack.length === 0;
var noScroll = ((_a = panelElement === null || panelElement === void 0 ? void 0 : panelElement.scrollTop) !== null && _a !== void 0 ? _a : 0) === 0 && this.wrapperScrollTop === 0;
return atRoot && noNav && noScroll;
};
SettingsPanel.prototype.maybeSaveCurrentState = function () {
return this.isDefaultPanelState() ? null : this.buildCurrentState();
};
SettingsPanel.prototype.restoreNavigationState = function (state) {
var _a;
var pages = this.getPages();
this.activePage = (_a = pages[state.activePageIndex]) !== null && _a !== void 0 ? _a : this.getRootPage();
this.navigationStack = state.navigationStackIndices.map(function (i) { return pages[i]; }).filter(Boolean);
this.updateActivePageClass();
this.onActivePageChangedEvent();
this.activePage.onActiveEvent();
this.getDomElement().get(0).scrollTop = state.scrollTop;
this.wrapperScrollTop = state.wrapperScrollTop;
};
SettingsPanel.prototype.navigateToPage = function (targetPage, sourcePage, direction, skipAnimation) {
this.activePage = targetPage;
if (direction === NavigationDirection.Forwards) {
this.navigationStack.push(targetPage);
}
else {
this.navigationStack.pop();
}
this.animateNavigation(targetPage, sourcePage, skipAnimation);
this.updateActivePageClass();
sourcePage.onInactiveEvent();
targetPage.onActiveEvent();
this.onActivePageChangedEvent();
};
/**
* @param targetPage
* @param sourcePage
* @param skipAnimation This is just an internal flag if we want to have an animation. It is set true when we reset
* the navigation within the onShow callback of the settingsPanel. In this case we don't want an actual animation but
* the recalculation of the dimension of the settingsPanel.
* This is independent of the pageTransitionAnimation flag.
*/
SettingsPanel.prototype.animateNavigation = function (targetPage, sourcePage, skipAnimation) {
if (!this.config.pageTransitionAnimation) {
return;
}
var settingsPanelDomElement = this.getDomElement();
var settingsPanelHTMLElement = this.getDomElement().get(0);
// get current dimension
var settingsPanelWidth = settingsPanelHTMLElement.scrollWidth;
var settingsPanelHeight = settingsPanelHTMLElement.scrollHeight;
// calculate target size of the settings panel
sourcePage.getDomElement().css('display', 'none');
this.getDomElement().css({ width: '', height: '' }); // let css auto settings kick in again
var targetPageHtmlElement = targetPage.getDomElement().get(0);
// clone the targetPage DOM element so that we can calculate the width / height how they will be after
// switching the page. We are using a clone to prevent (mostly styling) side-effects on the real DOM element
var clone = targetPageHtmlElement.cloneNode(true);
// append to parent so we get the 'real' size
var containerWrapper = targetPageHtmlElement.parentNode;
containerWrapper.appendChild(clone);
// set clone visible
clone.style.display = 'block';
// collect target dimension
var targetSettingsPanelWidth = settingsPanelHTMLElement.scrollWidth;
var targetSettingsPanelHeight = settingsPanelHTMLElement.scrollHeight;
// remove clone from the DOM
clone.parentElement.removeChild(clone); // .remove() is not working in IE
sourcePage.getDomElement().css('display', '');
// set the values back to the current ones that the browser animates it (browsers don't animate 'auto' values)
settingsPanelDomElement.css({
width: settingsPanelWidth + 'px',
height: settingsPanelHeight + 'px',
});
if (!skipAnimation) {
// We need to force the browser to reflow between setting the width and height that we actually get a animation
this.forceBrowserReflow();
}
// set the values to the target dimension
settingsPanelDomElement.css({
width: targetSettingsPanelWidth + 'px',
height: targetSettingsPanelHeight + 'px',
});
};
SettingsPanel.prototype.forceBrowserReflow = function () {
// Force the browser to reflow the layout
// https://gist.github.com/paulirish/5d52fb081b3570c81e3a
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.getDomElement().get(0).offsetLeft;
};
/**
* Workaround for IE, Firefox and Safari
* when the settings panel fades out while an item of a select box is still hovered, the select box will not fade out
* while the settings panel does. This would leave a floating select box, which is just weird
*/
SettingsPanel.prototype.hideHoveredSelectBoxes = function () {
this.getComputedItems()
.map(function (item) { return item['settingComponent']; })
.filter(function (component) { return component instanceof SelectBox_1.SelectBox; })
.forEach(function (selectBox) { return selectBox.closeDropdown(); });
};
// collect all items from all pages (see hideHoveredSelectBoxes)
SettingsPanel.prototype.getComputedItems = function () {
var allItems = [];
for (var _i = 0, _a = this.getPages(); _i < _a.length; _i++) {
var page = _a[_i];
allItems.push.apply(allItems, page.getItems());
}
return allItems;
};
SettingsPanel.prototype.onSettingsStateChangedEvent = function () {
this.settingsPanelEvents.onSettingsStateChanged.dispatch(this);
};
SettingsPanel.prototype.onActivePageChangedEvent = function () {
this.settingsPanelEvents.onActivePageChanged.dispatch(this);
};
SettingsPanel.CLASS_ACTIVE_PAGE = 'active';
return SettingsPanel;
}(Container_1.Container));
exports.SettingsPanel = SettingsPanel;