UNPKG

bitmovin-player-ui

Version:
202 lines (177 loc) 6.54 kB
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(); } } }