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[]; } /** * 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
(that can be configured by the config) and an inner wrapper *
that contains the components. This double-
-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: * *
*
* ... child components ... *
*
*
* * @category Components */ export class Container extends Component { /** * A reference to the inner element that contains the components of the container. */ protected innerContainerElement: DOM; private componentsToAppend: Component[]; private componentsToPrepend: Component[]; private componentsToRemove: Component[]; 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) { 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) { 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): 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[]} */ getComponents(): Component[] { 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; 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
) 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
) 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(); } } }