Skip to content

Source: src/ui/Title.js

import DomSmith from '../../lib/dom/DomSmith.js';

/**
 * The Title component displays the primary and secondary media titles (if available) above the player viewport.
 * It remains hidden when no title data is present or when the feature is disabled.
 * The component automatically updates based on media data and playback language.
 * @exports module:src/ui/Title
 * @requires lib/dom/DomSmith
 * @author   Frank Kudermann - alphanull
 * @version  1.0.0
 * @license  MIT
 */
export default class Title {

    /**
     * Configuration options for the Title component.
     * @type     {Object}
     * @property {boolean} [showSecondary=true]  Shows the secondary title.
     */
    #config = {
        showSecondary: true
    };

    /**
     * Reference to the main player instance.
     * @type {module:src/core/Player}
     */
    #player;

    /**
     * Holds tokens of subscriptions to player events, for later unsubscribe.
     * @type {number[]}
     */
    #subscriptions;

    /**
     * Holds tokens of additional subscriptions.
     * @type {number[]}
     */
    #subSecondary;

    /**
     * Secret key only known to the player instance and initialized components.
     * Used to be able to restrict access to API methods in conjunction with secure mode.
     * @type {symbol}
     */
    #apiKey;

    /**
     * DomSmith for the title container.
     * @type {module:lib/dom/DomSmith}
     */
    #dom;

    /**
     * Current media title.
     * @type {string}
     */
    #title;

    /**
     * Current secondary media title.
     */
    #titleSecondary;

    /**
     * Timeout id for resize debouncing.
     * @type {number}
     */
    #resizeId;

    /**
     * Creates an instance of the Title component.
     * @param {module:src/core/Player}           player            Reference to the VisionPlayer instance.
     * @param {module:src/controller/Controller} parent            Reference to the parent instance.
     * @param {Object}                           [options]         Additional options.
     * @param {symbol}                           [options.apiKey]  Token for extended access to the player API.
     */
    constructor(player, parent, { apiKey }) {

        this.#config = player.initConfig('title', this.#config);

        if (!this.#config) return [false];

        this.#player = player;
        this.#apiKey = apiKey;

        this.#dom = new DomSmith({
            _ref: 'wrapper',
            className: 'vip-title is-hidden',
            'data-sort': 20,
            _nodes: [{
                className: 'vip-title-inner',
                _nodes: [{
                    _tag: 'h2',
                    _nodes: [{
                        _ref: 'titlePrimaryText',
                        _text: ''
                    }]
                }, this.#config.showSecondary ? {
                    _tag: 'h3',
                    _nodes: [{
                        _ref: 'titleSecondaryText',
                        _text: ''
                    }]
                } : null]
            }]
        }, player.dom.getElement(apiKey));

        this.#subscriptions = [
            this.#player.subscribe('media/ready', this.#onMediaReady),
            this.#player.subscribe('data/ready', this.#onDataReady),
            this.#player.subscribe('data/nomedia', () => {
                this.#dom.titlePrimaryText.nodeValue = this.#player.locale.t('errors.data.header');
                this.#dom.titleSecondaryText.nodeValue = '';
            })
        ];

    }

    /**
     * Called once the media data is available. Extracts title and secondary title, and sets up UI show/hide if needed.
     * @param {module:src/core/Data~mediaItem} mediaItem                   Object containing media info.
     * @param {string|Object<string,string>}   [mediaItem.title]           The primary title (may be multilingual).
     * @param {string|Object<string,string>}   [mediaItem.titleSecondary]  The secondary title (multilingual).
     * @listens module:src/core/Data#data/ready
     */
    #onDataReady = ({ title, titleSecondary }) => {

        this.#player.unsubscribe(this.#subSecondary);

        if (title) {

            this.#title = title;
            this.#titleSecondary = titleSecondary;

            if (this.#player.getState('ui.visible')) this.#show();

            this.#subSecondary = [
                this.#player.subscribe('ui/show', this.#show),
                this.#player.subscribe('ui/hide', this.#hide),
                this.#player.subscribe('ui/resize', this.#resize)
            ];

        } else this.#hide();

    };

    /**
     * Called when the media is ready for playback. Translates and displays the title if not hidden.
     * @listens module:src/core/Media#media/ready
     */
    #onMediaReady = () => {

        const lang = this.#player.getConfig('locale.lang'),
              resolve = data => (typeof data === 'object' ? data[lang] ?? Object.values(data)[0] : data);

        this.#dom.titlePrimaryText.nodeValue = resolve(this.#title);

        if (this.#config.showSecondary) {
            this.#dom.titleSecondaryText.nodeValue = resolve(this.#titleSecondary) ?? '';
        }
    };

    /**
     * Hides the title.
     * @listens module:src/ui/UI#ui/show
     */
    #show = () => {

        this.#dom.wrapper.classList.remove('is-hidden');
        this.#resize();

    };

    /**
     * Shows the title.
     * @listens module:src/ui/UI#ui/hide
     */
    #hide = () => {

        this.#dom.wrapper.classList.add('is-hidden');
        this.#player.dom.getElement(this.#apiKey).style.setProperty('--vip-ui-blocked-top', '-1px');

    };

    /**
     * Called on UI resize events. Adjusts a CSS variable to place other UI elements below the title.
     * @listens module:src/ui/UI#ui/resize
     */
    #resize = () => {

        const doResize = () => {
            if (this.#dom.wrapper.classList.contains('is-hidden')) return;
            const blockedTop = this.#dom.wrapper.clientHeight + this.#dom.wrapper.offsetTop;
            this.#player.dom.getElement(this.#apiKey).style.setProperty('--vip-ui-blocked-top', `${blockedTop}px`);
        };

        clearTimeout(this.#resizeId);
        this.#resizeId = setTimeout(doResize, 100);

    };

    /**
     * This method removes all events, subscriptions and DOM nodes created by this component.
     */
    destroy() {

        clearTimeout(this.#resizeId);
        this.#dom.destroy();
        this.#player.unsubscribe(this.#subscriptions);
        this.#player.unsubscribe(this.#subSecondary);
        this.#player = this.#dom = this.#apiKey = null;

    }

}