media-controls.js   [plain text]


/*
 * Copyright (C) 2016 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

class MediaControls extends LayoutNode
{

    constructor({ width = 300, height = 150, layoutTraits = LayoutTraits.Unknown } = {})
    {
        super(`<div class="media-controls"></div>`);

        this._scaleFactor = 1;
        this._shouldCenterControlsVertically = false;

        this.width = width;
        this.height = height;
        this.layoutTraits = layoutTraits;

        this.playPauseButton = new PlayPauseButton(this);
        this.airplayButton = new AirplayButton(this);
        this.pipButton = new PiPButton(this);
        this.fullscreenButton = new FullscreenButton(this);
        this.muteButton = new MuteButton(this);
        this.tracksButton = new TracksButton(this);

        this.statusLabel = new StatusLabel(this);
        this.timeControl = new TimeControl(this);

        this.tracksPanel = new TracksPanel;

        this.bottomControlsBar = new ControlsBar("bottom");

        this.autoHideController = new AutoHideController(this);
        this.autoHideController.fadesWhileIdle = false;
        this.autoHideController.hasSecondaryUIAttached = false;

        this._placard = null;
        this.airplayPlacard = new AirplayPlacard(this);
        this.invalidPlacard = new InvalidPlacard(this);
        this.pipPlacard = new PiPPlacard(this);

        this.element.addEventListener("focusin", this);
        window.addEventListener("dragstart", this, true);
    }

    // Public

    get visible()
    {
        return super.visible;
    }

    set visible(flag)
    {
        if (this.visible === flag)
            return;

        // If we just got made visible again, let's fade the controls in.
        if (flag && !this.visible)
            this.faded = false;
        else if (!flag)
            this.autoHideController.mediaControlsBecameInvisible();

        super.visible = flag;

        if (flag)
            this.layout();

        if (this.delegate && typeof this.delegate.mediaControlsVisibilityDidChange === "function")
            this.delegate.mediaControlsVisibilityDidChange();
    }

    get faded()
    {
        return !!this._faded;
    }

    set faded(flag)
    {
        if (this._faded === flag)
            return;

        this._faded = flag;
        this.markDirtyProperty("faded");

        this.autoHideController.mediaControlsFadedStateDidChange();
        if (this.delegate && typeof this.delegate.mediaControlsFadedStateDidChange === "function")
            this.delegate.mediaControlsFadedStateDidChange();
    }

    get usesLTRUserInterfaceLayoutDirection()
    {
        return this.element.classList.contains("uses-ltr-user-interface-layout-direction");
    }

    set usesLTRUserInterfaceLayoutDirection(flag)
    {
        this.needsLayout = this.usesLTRUserInterfaceLayoutDirection !== flag;
        this.element.classList.toggle("uses-ltr-user-interface-layout-direction", flag);
    }

    get scaleFactor()
    {
        return this._scaleFactor;
    }

    set scaleFactor(scaleFactor)
    {
        if (this._scaleFactor === scaleFactor)
            return;

        this._scaleFactor = scaleFactor;
        this.markDirtyProperty("scaleFactor");
    }

    get shouldCenterControlsVertically()
    {
        return this._shouldCenterControlsVertically;
    }

    set shouldCenterControlsVertically(flag)
    {
        if (this._shouldCenterControlsVertically === flag)
            return;

        this._shouldCenterControlsVertically = flag;
        this.markDirtyProperty("scaleFactor");
    }

    get placard()
    {
        return this._placard;
    }

    set placard(placard)
    {
        if (this._placard === placard)
            return;

        this._placard = placard;
        this.layout();
    }

    placardPreventsControlsBarDisplay()
    {
        return this._placard && this._placard !== this.airplayPlacard;
    }

    showTracksPanel()
    {
        this.element.classList.add("shows-tracks-panel");

        this.tracksButton.on = true;
        this.tracksButton.element.blur();
        this.autoHideController.hasSecondaryUIAttached = true;
        this.tracksPanel.presentInParent(this);

        const controlsBounds = this.element.getBoundingClientRect();
        const controlsBarBounds = this.bottomControlsBar.element.getBoundingClientRect();
        const tracksButtonBounds = this.tracksButton.element.getBoundingClientRect();
        this.tracksPanel.rightX = this.width - (tracksButtonBounds.right - controlsBounds.left);
        this.tracksPanel.bottomY = this.height - (controlsBarBounds.top - controlsBounds.top) + 1;
        this.tracksPanel.maxHeight = this.height - this.tracksPanel.bottomY - 10;
    }

    hideTracksPanel()
    {
        this.element.classList.remove("shows-tracks-panel");

        let shouldFadeControlsBar = true;
        if (window.event instanceof MouseEvent)
            shouldFadeControlsBar = !this.isPointInControls(new DOMPoint(event.clientX, event.clientY), true);

        this.tracksButton.on = false;
        this.tracksButton.element.focus();
        this.autoHideController.hasSecondaryUIAttached = false;
        this.faded = this.autoHideController.fadesWhileIdle && shouldFadeControlsBar;
        this.tracksPanel.hide();
    }

    fadeIn()
    {
        this.element.classList.add("fade-in");
    }

    isPointInControls(point, includeContainer)
    {
        let ancestor = this.element.parentNode;
        while (ancestor && !(ancestor instanceof ShadowRoot))
            ancestor = ancestor.parentNode;

        const shadowRoot = ancestor;
        if (!shadowRoot)
            return false;

        const tappedElement = shadowRoot.elementFromPoint(point.x, point.y);

        if (includeContainer && this.element === tappedElement)
            return true;

        return this.children.some(child => child.element.contains(tappedElement));
    }

    // Protected

    handleEvent(event)
    {
        if (event.type === "focusin" && event.currentTarget === this.element)
            this.faded = false;
        else if (event.type === "dragstart" && this.isPointInControls(new DOMPoint(event.clientX, event.clientY)))
            event.preventDefault();
    }

    layout()
    {
        super.layout();

        if (this._placard) {
            this._placard.width = this.width;
            this._placard.height = this.height;
        }
    }

    commitProperty(propertyName)
    {
        if (propertyName === "scaleFactor") {
            const zoom = 1 / this._scaleFactor;
            // We want to maintain the controls at a constant device height. To do so, we invert the page scale
            // factor using a scale transform when scaling down, when the result will not appear pixelated and
            // where the CSS zoom property produces incorrect text rendering due to enforcing the minimum font
            // size. When we would end up scaling up, which would yield pixelation, we use the CSS zoom property
            // which will not run into the font size issue.
            if (zoom < 1) {
                this.element.style.transform = `scale(${zoom})`;
                this.element.style.removeProperty("zoom");
            } else {
                this.element.style.zoom = zoom;
                this.element.style.removeProperty("transform");
            }
            // We also want to optionally center them vertically compared to their container.
            this.element.style.top = this._shouldCenterControlsVertically ? `${(this.height / 2) * (zoom - 1)}px` : "auto"; 
        } else if (propertyName === "faded")
            this.element.classList.toggle("faded", this.faded);
        else
            super.commitProperty(propertyName);
    }

}