Source: player/FrameList.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** Show/hide the frame list.
 *
 * This module is part of the Sozi player embedded in each presentation.
 *
 * @module
 */

import {Animator} from "./Animator";
import * as Timing from "./Timing";

/** The duration of the open/close animation of the frame list.
 *
 * @readonly
 * @type {number} */
const DURATION_MS = 500;

/** The HTML element that contains the frame list.
 *
 * @type {HTMLElement} */
let frameList;

/** The HTML links to each frame in the frame list.
 *
 * @type {HTMLAnchorElement[]} */
let links;

/** The current Sozi player.
 *
 * @type {module:player/Player.Player} */
let player;

/** An animator to open/close the frame list.
 *
 * @type {module:player/Animator.Animator} */
let animator;

/** The current open status of the frame list.
 *
 * @default
 * @type {boolean} */
let isOpen = false;

/** The start location of the frame list with respect to the left border of the viewport.
 *
 * 1 represents the width of the frame list.
 *
 * @default
 * @type {number} */
let startOffset = -1;

/** The start location of the frame list with respect to the left border of the viewport.
 *
 * 1 represents the width of the frame list.
 *
 * @default
 * @type {number} */
let endOffset = -1;

/** The current location of the frame list with respect to the left border of the viewport.
 *
 * 1 represents the width of the frame list.
 *
 * @default
 * @type {number} */
let currentOffset = startOffset;

/** Initialize the frame list management.
 *
 * This function creates an {@linkcode module:player/Animator.Animator|animator}
 * to manage the open/close animations and registers its {@linkcode module:player/Animator.step|step}
 * even handler.
 *
 * It registers a {@linkcode module:player/Player.frameChange|frameChange} event handler
 * to highlight the current frame title in the frame list.
 *
 * It also registers mouse and keyboard events related to the frame list.
 *
 * @param {module:player/Player.Player} p - The current Sozi player.
 */
export function init(p) {
    player = p;

    frameList = document.querySelector(".sozi-frame-list");
    links = frameList.querySelectorAll("li a");

    for (let link of links) {
        link.addEventListener("click", evt => {
            if (evt.button === 0) {
                player.previewFrame(link.hash.slice(1));
                evt.preventDefault();
            }
        });
    }

    animator = new Animator();
    animator.on("step", onAnimatorStep);
    window.addEventListener("keypress", onKeyPress, false);
    window.addEventListener("resize", () => setCurrentOffset(currentOffset));
    player.viewport.on("mouseDown", onMouseDown);
    player.viewport.svgRoot.addEventListener("touchstart", evt => onTouchStart);
    frameList.addEventListener("mouseout", onMouseOut, false);
    p.on("frameChange", onFrameChange);
    setCurrentOffset(startOffset);
}

/** Set the location of the frame list with respect to the left border of the viewport.
 *
 * 1 represents the width of the frame list.
 *
 * @param {number} offset - The new location.
 */
function setCurrentOffset(offset) {
    currentOffset = offset;
    frameList.style.left = currentOffset * frameList.offsetWidth + "px";
}

/** Move the frame list to the given location, with an animation.
 *
 * 1 represents the width of the frame list.
 *
 * @param {number} offset - The target location.
 */
function moveTo(offset) {
    player.pause();
    startOffset = currentOffset;
    endOffset = offset;
    animator.start(Math.abs(endOffset - startOffset) * DURATION_MS);
}

/** Open the frame list. */
export function open() {
    moveTo(0);
}

/** Close the frame list. */
export function close() {
    moveTo(-1);
}

/** Toggle the open/closed status of the frame list. */
export function toggle() {
    moveTo(-1 - endOffset);
}

/** Process a keypress event related to the frame list.
 *
 * If enabled by the presentation, pressing the key `T` will toggle the open/close status of the frame list.
 *
 * @param {KeyboardEvent} evt - The DOM event representing the keypress.
 *
 * @listens keypress
 */
function onKeyPress(evt) {
    // Keys with modifiers are ignored
    if (evt.altKey || evt.ctrlKey || evt.metaKey) {
        return;
    }

    switch (evt.charCode || evt.which) {
        case 84: // T
        case 116: // t
            if (player.presentation.enableKeyboardNavigation) {
                player.disableBlankScreen();
                toggle();
            }
            break;
        default:
            return;
    }

    evt.stopPropagation();
    evt.preventDefault();
}

/** Perform an animation step while moving the frame list.
 *
 * This function is called by the current animator.
 *
 * @param {number} progress - The current progress indicator, between 0 and 1.
 *
 * @listens module:player/Animator.step
 */
function onAnimatorStep(progress) {
    const p = Timing.ease(progress);
    setCurrentOffset(endOffset * p + startOffset * (1 - p));
}

/** Process a mouse-down event.
 *
 * If enabled by the presentation, pressing the middle mouse button will
 * toggle the open/close status of the frame list.
 *
 * @param {number} button - The index of the button that was pressed.
 *
 * @listens module:player/Viewport.mouseDown
 */
function onMouseDown(button) {
    if (player.presentation.enableMouseNavigation && button === 1) {
        toggle();
    }
}

/** Process a touch event on touch devices.
 *
 * Closes the Frame List on any touch outside of the list itself.
 *
 * @param {TouchEvent} evt - the touch event.
 *
 * @listens touchstart
 */
function onTouchStart(evt){
    close();
}

/** Process a mouse-out event.
 *
 * When the mouse cursor moves out of the frame list area,
 * this function closes it.
 *
 * @param {MouseEvent} evt - The DOM event representing the mouse gesture.
 *
 * @listens mouseout
 */
function onMouseOut(evt) {
    let rel = evt.relatedTarget;
    while (rel && rel !== frameList && rel !== document.documentElement) {
        rel = rel.parentNode;
    }
    if (rel !== frameList) {
        close();
        evt.stopPropagation();
    }
}

/** Highlight the current frame in the frame list.
 *
 * @listens module:player/Player.frameChange
 */
function onFrameChange() {
    for (let link of links) {
        link.className = link.hash === "#" + player.currentFrame.frameId ?
            "current" :
            "";
    }
}