lokal-profil/mediawiki-extensions-Wikispeech

View on GitHub
modules/ext.wikispeech.ui.js

Summary

Maintainability
A
2 hrs
Test Coverage
( function ( mw, $ ) {

    /**
     * Creates and controls the UI for the extension.
     *
     * @class ext.wikispeech.Ui
     * @constructor
     */

    function Ui() {
        var self = this;

        /**
         * Initialize elements and functionality for the UI.
         */

        this.init = function () {
            mw.wikispeech.ui.addControlPanel();
            mw.wikispeech.ui.addSelectionPlayer();
            mw.wikispeech.ui.addStackToPlayStopButton();
            mw.wikispeech.ui.addKeyboardShortcuts();
        };

        /**
         * Add a panel with controls for for Wikispeech.
         *
         * The panel contains buttons for controlling playback and
         * links to related pages.
         */

        this.addControlPanel = function () {
            $( '<div></div>' )
                .attr( 'id', 'ext-wikispeech-control-panel' )
                .addClass( 'ext-wikispeech-control-panel' )
                .appendTo( '#content' );
            self.addButton(
                'ext-wikispeech-skip-back-sentence',
                mw.wikispeech.player.skipBackUtterance
            );
            self.addButton(
                'ext-wikispeech-skip-back-word',
                mw.wikispeech.player.skipBackToken
            );
            self.addButton(
                'ext-wikispeech-play-stop-button',
                mw.wikispeech.player.playOrStop
            );
            self.addButton(
                'ext-wikispeech-skip-ahead-word',
                mw.wikispeech.player.skipAheadToken
            );
            self.addButton(
                'ext-wikispeech-skip-ahead-sentence',
                mw.wikispeech.player.skipAheadUtterance
            );
            self.addLinkButton(
                'ext-wikispeech-help',
                'wgWikispeechHelpPage'
            );
            self.addLinkButton(
                'ext-wikispeech-feedback',
                'wgWikispeechFeedbackPage'
            );
            if (
                $( '.ext-wikispeech-help, ext-wikispeech-feedback' ).length
            ) {
                // Add divider if there are any non-control buttons.
                $( '<span></span>' )
                    .addClass( 'ext-wikispeech-divider' )
                    .insertBefore(
                        $( '.ext-wikispeech-help, ext-wikispeech-feedback' )
                            .first()
                    );
            }
        };

        /**
        * Add a control button.
        *
        * @param {string} cssClass The name of the CSS class to add to
        *  the button.
        * @param {string} onClickFunction The name of the function to
        *  call when the button is clicked.
        */

        this.addButton = function ( cssClass, onClickFunction ) {
            var $button = $( '<button></button>' )
                .addClass( cssClass )
                .appendTo( '#ext-wikispeech-control-panel' );
            $button.click( onClickFunction );
            return $button;
        };

        /**
         * Add a stack which contains the buffering icon to `playStopButton`s.
         */

        this.addStackToPlayStopButton = function () {
            this.addSpanToPlayStopButton();
            this.addElementToPlayStopButtonStack(
                'ext-wikispeech-play-stop ext-wikispeech-play fa-stack-2x'
            );
            this.addElementToPlayStopButtonStack(
                'ext-wikispeech-buffering-icon fa-stack-2x fa-spin'
            );
            $( '.ext-wikispeech-play-stop-stack' ).css( 'font-size', '50%' );
            $( '.ext-wikispeech-buffering-icon' ).css( 'visibility', 'hidden' );
        };

        /**
         * Add a Font Awesome stack to the play button.
         */

        this.addSpanToPlayStopButton = function () {
            $( '<span></span>' )
                .addClass( 'ext-wikispeech-play-stop-stack fa-stack fa-lg' )
                .appendTo( '.ext-wikispeech-play-stop-button' );
        };

        /**
         * Add an element to the stack on the playStop button.
         *
         * @param {string} cssClass The name of the CSS class to add
         *  the item.
         */

        this.addElementToPlayStopButtonStack = function ( cssClass ) {
            $( '<i></i>' )
                .addClass( 'fa ' + cssClass )
                .appendTo( '.ext-wikispeech-play-stop-stack' );
        };

        /**
         * Hide the buffering icon.
         */

        this.hideBufferingIcon = function () {
            $( '.ext-wikispeech-buffering-icon' )
                .css( 'visibility', 'hidden' );
        };

        /**
         * Show the buffering icon if the current audio is loading.
         */

        this.showBufferingIconIfAudioIsLoading = function ( audio ) {
            if ( self.audioIsReady( audio ) ) {
                self.hideBufferingIcon();
            } else {
                self.addCanPlayListener( $( audio ) );
                $( '.ext-wikispeech-buffering-icon' )
                    .css( 'visibility', 'visible' );
            }
        };

        /**
         * Check if the current audio is ready to play.
         *
         * The audio is deemed ready to play as soon as any playable
         * data is available.
         *
         * @param {HTMLElement} audio The audio element to test.
         * @return {boolean} True if the audio is ready to play else false.
         */

        this.audioIsReady = function ( audio ) {
            return audio.readyState >= 2;
        };

        /**
         * Add canplay listener for the audio to hide buffering icon.
         *
         * Canplaythrough will be caught implicitly as it occurs after
         * canplay.
         *
         * @param {jQuery} $audioElement Audio element to which the
         *  listener is added.
         */

        this.addCanPlayListener = function ( $audioElement ) {
            $audioElement.on( 'canplay', function () {
                $( '.ext-wikispeech-buffering-icon' )
                    .css( 'visibility', 'hidden' );
            } );
        };

        /**
         * Remove canplay listener for the audio to hide buffering icon.
         *
         * @param {jQuery} $audioElement Audio element from which the
         *  listener is removed.
         */

        this.removeCanPlayListener = function ( $audioElement ) {
            $audioElement.off( 'canplay' );
        };

        /**
         * Change the icon of the play/stop button to stop.
         */

        this.setPlayStopIconToStop = function () {
            $( '.ext-wikispeech-play-stop' ).addClass( 'ext-wikispeech-stop' );
            $( '.ext-wikispeech-play-stop' )
                .removeClass( 'ext-wikispeech-play' );
        };

        /**
         * Change the icon of the play/stop button to play.
         */

        this.setPlayStopIconToPlay = function () {
            $( '.ext-wikispeech-play-stop' ).addClass( 'ext-wikispeech-play' );
            $( '.ext-wikispeech-play-stop' )
                .removeClass( 'ext-wikispeech-stop' );
        };

        /**
        * Add a button that takes the user to another page.
        *
        * The button gets the link destination from a supplied
        * config variable. If the variable isn't specified, the button
        * isn't added.
        *
        * @param {string} cssClass The name of the CSS class to add to
        *  the button.
        * @param {string} configVariable The config variable to get
        *  link destination from.
        */

        this.addLinkButton = function ( cssClass, configVariable ) {
            var page, pagePath;

            page = mw.config.get( configVariable );
            if ( page ) {
                pagePath = mw.config.get( 'wgArticlePath' )
                    .replace( '$1', page );
                $( '<a></a>' )
                    .attr( 'href', pagePath )
                    .append(
                        $( '<button></button>' )
                            .addClass( cssClass )
                    )
                    .appendTo( '#ext-wikispeech-control-panel' );
            }
        };

        /**
         * Add a small player that appears when text is selected.
         */

        this.addSelectionPlayer = function () {
            var $player = $( '<div></div>' )
                .addClass( 'ext-wikispeech-selection-player' )
                .appendTo( '#content' );
            $( '<button></button>' )
                .addClass( 'ext-wikispeech-play-stop-button' )
                .click( mw.wikispeech.player.playOrStop )
                .appendTo( $player );
            $( document ).on( 'mouseup', function () {
                if ( mw.wikispeech.selectionPlayer.isSelectionValid() ) {
                    self.showSelectionPlayer();
                } else {
                    $( '.ext-wikispeech-selection-player' )
                        .css( 'visibility', 'hidden' );
                }
            } );
            $( document ).on( 'click', function () {
                // A click listener is also needed because of the
                // order of events when text is deselected by clicking
                // it.
                if ( !mw.wikispeech.selectionPlayer.isSelectionValid() ) {
                    $( '.ext-wikispeech-selection-player' )
                        .css( 'visibility', 'hidden' );
                }
            } );
        };

        /**
         * Show the selection player below the end of the selection.
         */

        this.showSelectionPlayer = function () {
            var selection, lastRange, lastRect, left, top;

            selection = window.getSelection();
            lastRange = selection.getRangeAt( selection.rangeCount - 1 );
            lastRect =
                mw.wikispeech.util.getLast( lastRange.getClientRects() );
            // Place the player under the end of the selected text.
            if ( self.getTextDirection( lastRange.endContainer ) === 'rtl' ) {
                // For RTL languages, the end of the text is the far left.
                left = lastRect.left + $( document ).scrollLeft();
            } else {
                // For LTR languages, the end of the text is the far
                // right. This is the default value for the direction
                // property.
                left =
                    lastRect.right +
                    $( document ).scrollLeft() -
                    $( '.ext-wikispeech-selection-player' ).width();
            }
            $( '.ext-wikispeech-selection-player' ).css( 'left', left );
            top = lastRect.bottom + $( document ).scrollTop();
            $( '.ext-wikispeech-selection-player' ).css( 'top', top );
            $( '.ext-wikispeech-selection-player' )
                .css( 'visibility', 'visible' );
        };

        /**
         * Get the text direction for a node.
         *
         * @return {string} The CSS value of the `direction` property
         *  for the node, or for its parent if it is a text node.
         */

        this.getTextDirection = function ( node ) {
            if ( node.nodeType === 3 ) {
                // For text nodes, get the property of the parent element.
                return $( node ).parent().css( 'direction' );
            } else {
                return $( node ).css( 'direction' );
            }
        };

        /**
         * Register listeners for keyboard shortcuts.
         */

        this.addKeyboardShortcuts = function () {
            var shortcuts, name, shortcut;

            shortcuts = mw.config.get( 'wgWikispeechKeyboardShortcuts' );
            $( document ).keydown( function ( event ) {
                if ( self.eventMatchShortcut( event, shortcuts.playStop ) ) {
                    mw.wikispeech.player.playOrStop();
                    return false;
                } else if (
                    self.eventMatchShortcut(
                        event,
                        shortcuts.skipAheadSentence
                    )
                ) {
                    mw.wikispeech.player.skipAheadUtterance();
                    return false;
                } else if (
                    self.eventMatchShortcut(
                        event,
                        shortcuts.skipBackSentence
                    )
                ) {
                    mw.wikispeech.player.skipBackUtterance();
                    return false;
                } else if (
                    self.eventMatchShortcut( event, shortcuts.skipAheadWord )
                ) {
                    mw.wikispeech.player.skipAheadToken();
                    return false;
                } else if (
                    self.eventMatchShortcut( event, shortcuts.skipBackWord )
                ) {
                    mw.wikispeech.player.skipBackToken();
                    return false;
                }
            } );
            // Prevent keyup events from triggering if there is
            // keydown event for the same key combination. This caused
            // buttons in focus to trigger if a shortcut had space as
            // key.
            $( document ).keyup( function ( event ) {
                for ( name in shortcuts ) {
                    shortcut = shortcuts[ name ];
                    if ( self.eventMatchShortcut( event, shortcut ) ) {
                        event.preventDefault();
                    }
                }
            } );
        };

        /**
         * Check if a keydown event matches a shortcut from the
         * configuration.
         *
         * Compare the key and modifier state (of ctrl, alt and shift)
         * for an event, to those of a shortcut from the
         * configuration.
         *
         * @param {Event} event The event to compare.
         * @param {Object} shortcut The shortcut object from the
         *  config to compare to.
         * @return {boolean} true if key and all the modifiers match
         *  with the shortcut, else false.
         */

        this.eventMatchShortcut = function ( event, shortcut ) {
            return event.which === shortcut.key &&
                event.ctrlKey === shortcut.modifiers.indexOf( 'ctrl' ) >= 0 &&
                event.altKey === shortcut.modifiers.indexOf( 'alt' ) >= 0 &&
                event.shiftKey === shortcut.modifiers.indexOf( 'shift' ) >= 0;
        };
    }

    mw.wikispeech = mw.wikispeech || {};
    mw.wikispeech.Ui = Ui;
    mw.wikispeech.ui = new Ui();
}( mediaWiki, jQuery ) );