tracks-panel.js   [plain text]



class TracksPanel extends LayoutNode
{

    constructor()
    {
        super(`<div class="tracks-panel"></div>`);
        this._backgroundTint = new BackgroundTint;
        this._scrollableContainer = new LayoutNode(`<div class="scrollable-container"></div>`);
        this._rightX = 0;
        this._bottomY = 0;
        this._presented = false;
        
        this.children = [this._backgroundTint, this._scrollableContainer];
    }

    // Public

    get presented()
    {
        return this._presented;
    }

    presentInParent(node)
    {
        if (this._presented && this.parent === node)
            return;

        this._presented = true;

        this._scrollableContainer.children = this._childrenFromDataSource();

        node.addChild(this);

        this.element.removeEventListener("transitionend", this);
        this.element.classList.remove("fade-out");

        this._mousedownTarget().addEventListener("mousedown", this, true);
        window.addEventListener("keydown", this, true);

        this._focusedTrackNode = null;
    }

    hide()
    {
        if (!this._presented)
            return;

        this._presented = false;

        this._mousedownTarget().removeEventListener("mousedown", this, true);
        window.removeEventListener("keydown", this, true);

        this.element.addEventListener("transitionend", this);

        // Ensure a transition will indeed happen by starting it only on the next frame.
        window.requestAnimationFrame(() => { this.element.classList.add("fade-out"); });
    }

    get maxHeight()
    {
        return this._maxHeight;
    }

    set maxHeight(height)
    {
        if (this._maxHeight === height)
            return;

        this._maxHeight = height;
        this.markDirtyProperty("maxHeight")
    }

    get bottomY()
    {
        return this._bottomY;
    }

    set bottomY(bottomY)
    {
        if (this._bottomY === bottomY)
            return;

        this._bottomY = bottomY;
        this.markDirtyProperty("bottomY");
    }

    get rightX()
    {
        return this._rightX;
    }

    set rightX(x)
    {
        if (this._rightX === x)
            return;

        this._rightX = x;
        this.markDirtyProperty("rightX");
    }

    // Protected

    trackNodeSelectionAnimationDidEnd(trackNode)
    {
        if (this.uiDelegate && typeof this.uiDelegate.tracksPanelSelectionDidChange === "function")
            this.uiDelegate.tracksPanelSelectionDidChange(trackNode.index, trackNode.sectionIndex);
    }

    mouseMovedOverTrackNode(trackNode)
    {
        this._focusTrackNode(trackNode);
    }

    mouseExitedTrackNode(trackNode)
    {
        this._focusedTrackNode.element.blur();
        delete this._focusedTrackNode;
    }

    commitProperty(propertyName)
    {
        if (propertyName === "rightX")
            this.element.style.right = `${this._rightX}px`;
        else if (propertyName === "bottomY")
            this.element.style.bottom = `${this._bottomY}px`;
        else if (propertyName === "maxHeight") {
            this.element.style.maxHeight = `${this._maxHeight}px`;
            this._scrollableContainer.element.style.maxHeight = `${this._maxHeight}px`;
        } else
            super.commitProperty(propertyName);
    }

    handleEvent(event)
    {
        switch (event.type) {
        case "mousedown":
            this._handleMousedown(event);
            break;
        case "keydown":
            this._handleKeydown(event);
            break;
        case "transitionend":
            this.remove();
            break;
        }
    }

    // Private

    _mousedownTarget()
    {
        const mediaControls = this.parentOfType(MacOSFullscreenMediaControls);
        if (mediaControls)
            return mediaControls.element;
        return window;
    }

    _childrenFromDataSource()
    {
        const children = [];

        this._trackNodes = [];

        const dataSource = this.dataSource;
        if (!dataSource)
            return children;

        const numberOfSections = dataSource.tracksPanelNumberOfSections();
        if (numberOfSections === 0)
            return children;

        for (let sectionIndex = 0; sectionIndex < numberOfSections; ++sectionIndex) {
            let sectionNode = new LayoutNode(`<section></section>`);
            sectionNode.addChild(new LayoutNode(`<h3>${dataSource.tracksPanelTitleForSection(sectionIndex)}</h3>`));

            let tracksListNode = sectionNode.addChild(new LayoutNode(`<ul></ul>`));
            let numberOfTracks = dataSource.tracksPanelNumberOfTracksInSection(sectionIndex);
            for (let trackIndex = 0; trackIndex < numberOfTracks; ++trackIndex) {
                let trackTitle = dataSource.tracksPanelTitleForTrackInSection(trackIndex, sectionIndex);
                let trackSelected = dataSource.tracksPanelIsTrackInSectionSelected(trackIndex, sectionIndex);
                let trackNode = tracksListNode.addChild(new TrackNode(trackIndex, sectionIndex, trackTitle, trackSelected, this));
                this._trackNodes.push(trackNode);
            }
            children.push(sectionNode);
        }

        return children;
    }

    _handleMousedown(event)
    {
        if (this._isPointInTracksPanel(new DOMPoint(event.clientX, event.clientY)))
            return;

        this._dismiss();

        event.preventDefault();
        event.stopPropagation();
    }

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

        if (!ancestor)
            ancestor = document;

        return this.element.contains(ancestor.elementFromPoint(point.x, point.y));
    }

    _handleKeydown(event)
    {
        switch (event.key) {
        case "Home":
        case "PageUp":
            this._focusFirstTrackNode();
            break;
        case "End":
        case "PageDown":
            this._focusLastTrackNode();
            break;
        case "ArrowDown":
            if (event.altKey || event.metaKey)
                this._focusLastTrackNode();
            else
                this._focusNextTrackNode();
            break;
        case "ArrowUp":
            if (event.altKey || event.metaKey)
                this._focusFirstTrackNode();
            else
                this._focusPreviousTrackNode();
            break;
        case " ":
        case "Enter":
            if (this._focusedTrackNode)
                this._focusedTrackNode.activate();
            break;
        case "Escape":
            this._dismiss();
            break;
        default:
            return;
        }

        // Ensure that we don't let the browser react to a key code we handled,
        // for instance scrolling the page if we handled an arrow key.
        event.preventDefault();
    }

    _dismiss()
    {
        if (this.parent && typeof this.parent.hideTracksPanel === "function")
            this.parent.hideTracksPanel();
    }

    _focusTrackNode(trackNode)
    {
        if (!trackNode || trackNode === this._focusedTrackNode)
            return;

        trackNode.element.focus();
        this._focusedTrackNode = trackNode;
    }

    _focusPreviousTrackNode()
    {
        const previousIndex = this._focusedTrackNode ? this._trackNodes.indexOf(this._focusedTrackNode) - 1 : this._trackNodes.length - 1;
        this._focusTrackNode(this._trackNodes[previousIndex]);
    }

    _focusNextTrackNode()
    {
        this._focusTrackNode(this._trackNodes[this._trackNodes.indexOf(this._focusedTrackNode) + 1]);
    }

    _focusFirstTrackNode()
    {
        this._focusTrackNode(this._trackNodes[0]);
    }

    _focusLastTrackNode()
    {
        this._focusTrackNode(this._trackNodes[this._trackNodes.length - 1]);
    }

}

class TrackNode extends LayoutNode
{

    constructor(index, sectionIndex, title, selected, panel)
    {
        super(`<li tabindex="0">${title}</li>`);

        this.index = index;
        this.sectionIndex = sectionIndex;
        this._panel = panel;
        this._selected = selected;

        if (selected)
            this.element.classList.add("selected");

        this.element.addEventListener("mousemove", this);
        this.element.addEventListener("mouseleave", this);
        this.element.addEventListener("click", this);
    }

    // Public

    activate()
    {
        this.element.addEventListener("animationend", this);
        this.element.classList.add("animated");
    }

    // Protected

    handleEvent(event)
    {
        switch (event.type) {
        case "mousemove":
            this._panel.mouseMovedOverTrackNode(this);
            break;
        case "mouseleave":
            this._panel.mouseExitedTrackNode(this);
            break;
        case "click":
            this.activate();
            break;
        case "animationend":
            this._animationDidEnd();
            break;
        }
    }

    // Private

    _animationDidEnd()
    {
        this.element.removeEventListener("animationend", this);
        this._panel.trackNodeSelectionAnimationDidEnd(this);
    }

}