Skip to content

Source: src/ui/Spinner.js

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

/**
 * The Spinner component displays a "busy" animation when the player stalls—typically due to an empty buffer or network-related delay.
 * It appears with a configurable delay to avoid flickering during short interruptions.
 * The spinner listens to player stall events as well as manually published control events.
 * @exports module:src/ui/Spinner
 * @requires lib/dom/DomSmith
 * @author   Frank Kudermann - alphanull
 * @version  1.0.0
 * @license  MIT
 */
export default class Spinner {

    /**
     * Holds the instance configuration for this component.
     * @type     {Object}
     * @property {number} [delay=1]  Delay (in seconds) after which the spinner animation is shown. Used to prevent superfluous showing when the stall period is very short.
     */
    #config = {
        delay: 1
    };

    /**
     * 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;

    /**
     * A DomSmith instance used to create the spinner DOM.
     * @type {module:lib/dom/DomSmith}
     */
    #dom;

    /**
     * Spinner state (i.e. "visible" or "hidden") is stored here. Used to prevent double triggering hide() or show().
     * @type {string}
     */
    #state;

    /**
     * Holds the setTimeout id.
     * @type {number}
     */
    #timeOutId;

    /**
     * Creates an instance of the Spinner component.
     * @param {module:src/core/Player} player  Reference to the VisionPlayer instance.
     * @param {module:src/ui/UI}       parent  Reference to the parent instance, in this case the UI.
     */
    constructor(player, parent) {

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

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

        this.#player = player;
        this.#state = '';

        this.#dom = new DomSmith({
            _ref: 'wrapper',
            className: 'vip-spinner is-hidden',
            ariaHidden: true,
            'aria-role': 'presentation',
            _nodes: [{
                className: 'vip-spinner-wrapper',
                _nodes: [
                    { className: 'vip-spinner-item vip-spinner-item-1' },
                    { className: 'vip-spinner-item vip-spinner-item-2' }
                ]
            }]

        }, parent.getElement());

        this.#subscriptions = [
            ['media/stall/begin', this.#show],
            ['media/stall/end', this.#hide],
            ['spinner/show', this.#show],
            ['spinner/hide', this.#hide]
        ].map(([event, handler]) => this.#player.subscribe(event, handler));

    }

    /**
     * Shows the spinner after the configured delay, if not already visible.
     * @listens module:src/core/Media#media/stall/begin
     * @listens module:src/ui/Spinner#spinner/show
     */
    #show = () => {

        if (this.#state === 'visible') return;

        this.#state = 'visible';

        const doShow = () => this.#dom.wrapper.classList.remove('is-hidden');

        if (this.#config.delay) {
            clearTimeout(this.#timeOutId);
            this.#timeOutId = setTimeout(doShow, this.#config.delay * 1000);
        } else doShow();

    };

    /**
     * Hides the spinner, if not already hidden.
     * @listens module:src/core/Media#media/stall/end
     * @listens module:src/ui/Spinner#spinner/hide
     */
    #hide = () => {

        if (this.#state === 'hidden') return;

        this.#state = 'hidden';

        const doHide = () => this.#dom.wrapper.classList.add('is-hidden');

        if (this.#config.delay) {
            clearTimeout(this.#timeOutId);
            this.#timeOutId = setTimeout(doHide, this.#config.delay * 1000);
        } else doHide();

    };

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

        clearTimeout(this.#timeOutId);
        this.#dom.destroy();
        this.#player.unsubscribe(this.#subscriptions);
        this.#player = this.#dom = null;

    }

}

/**
 * The Spinner component listens for this event to show the spinner.
 * @event module:src/ui/Spinner#spinner/show
 */

/**
 * The Spinner component listens for this event to hide the spinner.
 * @event module:src/ui/Spinner#spinner/hide
 */