bitmovin-player-ui
Version:
Bitmovin Player UI Framework
202 lines (177 loc) • 6.54 kB
text/typescript
import { ComponentConfig, Component, ViewModeChangedEventArgs, ViewMode } from './Component';
import { DOM } from '../DOM';
import { ArrayUtils } from '../utils/ArrayUtils';
import { i18n } from '../localization/i18n';
/**
* Configuration interface for a {@link Container}.
*
* @category Configs
*/
export interface ContainerConfig extends ComponentConfig {
/**
* Child components of the container.
*/
components?: Component<ComponentConfig>[];
}
/**
* A container component that can contain a collection of child components.
* Components can be added at construction time through the {@link ContainerConfig#components} setting, or later
* through the {@link Container#addComponent} method. The UIManager automatically takes care of all components, i.e. it
* initializes and configures them automatically.
*
* In the DOM, the container consists of an outer <div> (that can be configured by the config) and an inner wrapper
* <div> that contains the components. This double-<div>-structure is often required to achieve many advanced effects
* in CSS and/or JS, e.g. animations and certain formatting with absolute positioning.
*
* DOM example:
* <code>
* <div class='ui-container'>
* <div class='container-wrapper'>
* ... child components ...
* </div>
* </div>
* </code>
*
* @category Components
*/
export class Container<Config extends ContainerConfig> extends Component<Config> {
/**
* A reference to the inner element that contains the components of the container.
*/
protected innerContainerElement: DOM;
private componentsToAppend: Component<ComponentConfig>[];
private componentsToPrepend: Component<ComponentConfig>[];
private componentsToRemove: Component<ComponentConfig>[];
private componentsInPersistentViewMode: number;
constructor(config: Config) {
super(config);
this.config = this.mergeConfig(
config,
{
cssClass: 'ui-container',
components: [],
} as Config,
this.config,
);
this.componentsToAppend = [];
this.componentsToPrepend = [];
this.componentsToRemove = [];
this.componentsInPersistentViewMode = 0;
}
/**
* Adds a child component to the container.
* @param component the component to add
*/
addComponent(component: Component<ComponentConfig>) {
this.config.components.push(component);
this.componentsToAppend.push(component);
}
/**
* Adds a child component as the first component in the container.
* @param component the component to add
*/
prependComponent(component: Component<ComponentConfig>) {
this.config.components.unshift(component);
this.componentsToPrepend.push(component);
}
/**
* Removes a child component from the container.
* @param component the component to remove
* @returns {boolean} true if the component has been removed, false if it is not contained in this container
*/
removeComponent(component: Component<ComponentConfig>): boolean {
if (ArrayUtils.remove(this.config.components, component) != null) {
this.componentsToRemove.push(component);
return true;
} else {
return false;
}
}
/**
* Gets an array of all child components in this container.
* @returns {Component<ComponentConfig>[]}
*/
getComponents(): Component<ComponentConfig>[] {
return this.config.components;
}
/**
* Removes all child components from the container.
*/
removeComponents(): void {
for (const component of this.getComponents().slice()) {
this.removeComponent(component);
}
}
/**
* Updates the DOM of the container with the current components.
*
* This is called automatically after construction. However, when you dynamically
* add or remove components at runtime, you must call `updateComponents()` to
* re-render the container’s children.
*/
updateComponents(): void {
/* We cannot just clear the container to remove all elements and then re-add those that should stay, because
* IE looses the innerHTML of unattached elements, leading to empty elements within the container (e.g. missing
* subtitle text in SubtitleLabel).
* Instead, we keep a list of elements to add and remove, leaving remaining elements alone. By keeping them in
* the DOM, their content gets preserved in all browsers.
*/
let component: Component<ComponentConfig>;
while ((component = this.componentsToRemove.shift()) !== undefined) {
component.getDomElement().remove();
}
while ((component = this.componentsToAppend.shift()) !== undefined) {
this.innerContainerElement.append(component.getDomElement());
}
while ((component = this.componentsToPrepend.shift()) !== undefined) {
this.innerContainerElement.prepend(component.getDomElement());
}
}
protected toDomElement(): DOM {
// Create the container element (the outer <div>)
const containerElement = new DOM(
this.config.tag,
{
id: this.config.id,
class: this.getCssClasses(),
role: this.config.role,
'aria-label': i18n.performLocalization(this.config.ariaLabel),
},
this,
);
if (typeof this.config.tabIndex === 'number') {
containerElement.attr('tabindex', this.config.tabIndex.toString());
}
// Create the inner container element (the inner <div>) that will contain the components
const innerContainer = new DOM(this.config.tag, {
class: this.prefixCss('container-wrapper'),
});
this.innerContainerElement = innerContainer;
for (const initialComponent of this.config.components) {
this.componentsToAppend.push(initialComponent);
}
this.updateComponents();
containerElement.append(innerContainer);
return containerElement;
}
protected suspendHideTimeout(): void {
// to be implemented in subclass
}
protected resumeHideTimeout(): void {
// to be implemented in subclass
}
protected trackComponentViewMode(mode: ViewMode) {
if (mode === ViewMode.Persistent) {
this.componentsInPersistentViewMode++;
} else if (mode === ViewMode.Temporary) {
this.componentsInPersistentViewMode = Math.max(this.componentsInPersistentViewMode - 1, 0);
}
if (this.componentsInPersistentViewMode > 0) {
// There is at least one component that must not be hidden,
// therefore the hide timeout must be suspended
this.suspendHideTimeout();
} else {
this.resumeHideTimeout();
}
}
}