/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, CodeMirror, window */

/**
 * EditorManager owns the UI for the editor area. This essentially mirrors the 'current document'
 * property maintained by DocumentManager's model.
 *
 * Note that there is a little bit of unusual overlap between EditorManager and DocumentManager:
 * because the Document state is actually stored in the CodeMirror editor UI, DocumentManager is
 * not a pure headless model. Each Document encapsulates an editor instance, and thus EditorManager
 * must have some knowledge about Document's internal state (we access its _editor property).
 *
 * This module dispatches the following events:
 *    - activeEditorChange --  Fires after the active editor (full or inline) changes and size/visibility
 *                             are complete. Doesn't fire when editor temporarily loses focus to a non-editor
 *                             control (e.g. search toolbar or modal dialog, or window deactivation). Does
 *                             fire when focus moves between inline editor and its full-size container.
 *                             This event tracks getActiveEditor() changes, while DocumentManager's
 *                             currentDocumentChange tracks getCurrentFullEditor() changes.
 *                             The 2nd arg to the listener is which Editor became active; the 3rd arg is
 *                             which Editor is deactivated as a result. Either one may be null.
 *                             NOTE (#1257): getFocusedEditor() sometimes lags behind this event. Listeners
 *                             should use the arguments or call getActiveEditor() to reliably see which Editor
 *                             just gained focus.
 */
define(function (require, exports, module) {
    "use strict";

    // Load dependent modules
    var AppInit             = require("utils/AppInit"),
        FileUtils           = require("file/FileUtils"),
        Commands            = require("command/Commands"),
        CommandManager      = require("command/CommandManager"),
        DocumentManager     = require("document/DocumentManager"),
        PerfUtils           = require("utils/PerfUtils"),
        Editor              = require("editor/Editor").Editor,
        InlineTextEditor    = require("editor/InlineTextEditor").InlineTextEditor,
        KeyEvent            = require("utils/KeyEvent"),
        EditorUtils         = require("editor/EditorUtils"),
        ViewUtils           = require("utils/ViewUtils"),
        StatusBar           = require("widgets/StatusBar"),
        Strings             = require("strings"),
        StringUtils         = require("utils/StringUtils");

    /** @type {jQueryObject} DOM node that contains all editors (visible and hidden alike) */
    var _editorHolder = null;

    /**
     * Currently visible full-size Editor, or null if no editors open
     * @type {?Editor}
     */
    var _currentEditor = null;
    /** @type {?Document} */
    var _currentEditorsDocument = null;

    /**
     * Currently focused Editor (full-size, inline, or otherwise)
     * @type {?Editor}
     */
    var _lastFocusedEditor = null;

    /**
     * Registered inline-editor widget providers. See {@link #registerInlineEditProvider()}.
     * @type {Array.<function(...)>}
     */
    var _inlineEditProviders = [];

    /* StatusBar indicators */
    var $modeInfo,
        $cursorInfo,
        $fileInfo,
        $indentType,
        $indentWidthLabel,
        $indentWidthInput;

    /**
     * Adds keyboard command handlers to an Editor instance.
     * @param {Editor} editor
     * @param {!Object.<string,function(Editor)>} to destination key mapping
     * @param {!Object.<string,function(Editor)>} from source key mapping
     */
    function mergeExtraKeys(editor, to, from) {
        // Merge in the additionalKeys we were passed
        function wrapEventHandler(externalHandler) {
            return function (instance) {
                externalHandler(editor);
            };
        }
        var key;
        for (key in from) {
            if (from.hasOwnProperty(key)) {
                if (to.hasOwnProperty(key)) {
                    console.log("Warning: overwriting standard Editor shortcut " + key);
                }
                to[key] = (editor !== null) ? wrapEventHandler(from[key]) : from[key];
            }
        }
    }

    /**
     * Creates a new Editor bound to the given Document. The editor's mode is inferred based on the
     * file extension. The editor is appended to the given container as a visible child.
     * @param {!Document} doc  Document for the Editor's content
     * @param {!boolean} makeMasterEditor  If true, the Editor will set itself as the private "master"
     *          Editor for the Document. If false, the Editor will attach to the Document as a "slave."
     * @param {!jQueryObject} container  Container to add the editor to.
     * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document
     *          to display in this editor. Inclusive.
     * @return {Editor} the newly created editor.
     */
    function _createEditorForDocument(doc, makeMasterEditor, container, range, additionalKeys) {
        var mode = EditorUtils.getModeFromFileExtension(doc.file.fullPath);

        return new Editor(doc, makeMasterEditor, mode, container, additionalKeys, range);
    }

    /**
     * @private
     * Bound to Ctrl+E on outermost editors.
     * @param {!Editor} editor the candidate host editor
     * @return {$.Promise} a promise that will be resolved when an InlineWidget
     *      is created or rejected when no inline editors are available.
     */
    function _openInlineWidget(editor) {
        PerfUtils.markStart(PerfUtils.INLINE_EDITOR_OPEN);

        // Run through inline-editor providers until one responds
        var pos = editor.getCursorPos(),
            inlinePromise,
            i,
            result = new $.Deferred();

        for (i = 0; i < _inlineEditProviders.length && !inlinePromise; i++) {
            var provider = _inlineEditProviders[i];
            inlinePromise = provider(editor, pos);
        }

        // If one of them will provide a widget, show it inline once ready
        if (inlinePromise) {
            inlinePromise.done(function (inlineWidget) {
                editor.addInlineWidget(pos, inlineWidget);
                PerfUtils.addMeasurement(PerfUtils.INLINE_EDITOR_OPEN);
                result.resolve();
            }).fail(function () {
                // terminate timer that was started above
                PerfUtils.finalizeMeasurement(PerfUtils.INLINE_EDITOR_OPEN);
                result.reject();
            });
        } else {
            // terminate timer that was started above
            PerfUtils.finalizeMeasurement(PerfUtils.INLINE_EDITOR_OPEN);
            result.reject();
        }

        return result.promise();
    }

    /**
     * Removes the given widget UI from the given hostEditor (agnostic of what the widget's content
     * is). The widget's onClosed() callback will be run as a result.
     * @param {!Editor} hostEditor The editor containing the widget.
     * @param {!InlineWidget} inlineWidget The inline widget to close.
     * @param {!boolean} moveFocus  If true, focuses hostEditor and ensures the cursor position lies
     *      near the inline's location.
     */
    function closeInlineWidget(hostEditor, inlineWidget, moveFocus) {
        if (moveFocus) {
            // Place cursor back on the line just above the inline (the line from which it was opened)
            // If cursor's already on that line, leave it be to preserve column position
            var widgetLine = hostEditor._codeMirror.getInlineWidgetInfo(inlineWidget.id).line;
            var cursorLine = hostEditor.getCursorPos().line;
            if (cursorLine !== widgetLine) {
                hostEditor.setCursorPos({ line: widgetLine, pos: 0 });
            }

            hostEditor.focus();
        }

        hostEditor.removeInlineWidget(inlineWidget);
    }

    /**
     * Registers a new inline provider. When _openInlineWidget() is called each registered inline
     * widget is called and asked if it wants to provide an inline widget given the current cursor
     * location and document.
     * @param {function} provider
     *      Parameters:
     *      {!Editor} editor, {!{line:Number, ch:Number}} pos
     *
     *      Returns:
     *      {$.Promise} a promise that will be resolved with an inlineWidget
     *      or null to indicate the provider doesn't create an editor in this case
     */
    function registerInlineEditProvider(provider) {
        _inlineEditProviders.push(provider);
    }

    /**
     * @private
     * Given a host editor, return a list of all Editors in all its open inline widgets. (Ignoring
     * any other inline widgets that might be open but don't contain Editors).
     * @param {!Editor} hostEditor
     * @return {Array.<Editor>}
     *
     */
    function getInlineEditors(hostEditor) {
        var inlineEditors = [];

        if (hostEditor) {
            hostEditor.getInlineWidgets().forEach(function (widget) {
                if (widget instanceof InlineTextEditor) {
                    inlineEditors = inlineEditors.concat(widget.editors);
                }
            });
        }

        return inlineEditors;
    }

    /**
     * @private
     * Creates a new "full-size" (not inline) Editor for the given Document, and sets it as the
     * Document's master backing editor. The editor is not yet visible; to show it, use
     * DocumentManager.setCurrentDocument().
     * Semi-private: should only be called within this module or by Document.
     * @param {!Document} document  Document whose main/full Editor to create
     */
    function _createFullEditorForDocument(document) {
        // Create editor; make it initially invisible
        var container = _editorHolder.get(0);
        var editor = _createEditorForDocument(document, true, container);
        editor.setVisible(false);
    }

    /** Returns the visible full-size Editor corresponding to DocumentManager.getCurrentDocument() */
    function getCurrentFullEditor() {
        // This *should* always be equivalent to DocumentManager.getCurrentDocument()._masterEditor
        return _currentEditor;
    }

    /**
     * Creates a new inline Editor instance for the given Document. The editor's mode is inferred
     * based on the file extension. The editor is not yet visible or attached to a host editor.
     * @param {!Document} doc  Document for the Editor's content
     * @param {?{startLine:Number, endLine:Number}} range  If specified, all lines outside the given
     *      range are hidden from the editor. Range is inclusive. Line numbers start at 0.
     * @param {HTMLDivContainer} inlineContent
     * @param  {function(inlineWidget)} closeThisInline
     *
     * @return {{content:DOMElement, editor:Editor}}
     */
    function createInlineEditorForDocument(doc, range, inlineContent, additionalKeys) {
        // Create the Editor
        var inlineEditor = _createEditorForDocument(doc, false, inlineContent, range, additionalKeys);

        return { content: inlineContent, editor: inlineEditor };
    }

    /**
     * Disposes the given Document's full-size editor if the doc is no longer "open" from the user's
     * standpoint - not in the working set and not currentDocument).
     *
     * Destroying the full-size editor releases ONE ref to the Document; if inline editors or other
     * UI elements are still referencing the Document it will still be 'open' (kept alive) from
     * DocumentManager's standpoint. However, destroying the full-size editor does remove the backing
     * "master" editor from the Document, rendering it immutable until either inline-editor edits or
     * currentDocument change triggers _createFullEditorForDocument() full-size editor again.
     *
     * In certain edge cases, this is called directly by DocumentManager; see _gcDocuments() for details.
     *
     * @param {!Document} document Document whose "master" editor we may destroy
     */
    function _destroyEditorIfUnneeded(document) {
        var editor = document._masterEditor;

        if (!editor) {
            return;
        }

        // If outgoing editor is no longer needed, dispose it
        var isCurrentDocument = (DocumentManager.getCurrentDocument() === document);
        var isInWorkingSet = (DocumentManager.findInWorkingSet(document.file.fullPath) !== -1);
        if (!isCurrentDocument && !isInWorkingSet) {
            // Destroy the editor widget (which un-refs the Document and reverts it to read-only mode)
            editor.destroy();

            // Our callers should really ensure this, but just for safety...
            if (_currentEditor === editor) {
                _currentEditorsDocument = null;
                _currentEditor = null;
            }
        }
    }

    /**
     * Returns focus to the last visible editor that had focus. If no editor visible, does nothing.
     * This function should be called to restore editor focus after it has been temporarily
     * removed. For example, after a dialog with editable text is closed.
     */
    function focusEditor() {
        if (_lastFocusedEditor) {
            _lastFocusedEditor.focus();
        }
    }

    /**
     * Calculates the available height for the full-size Editor (or the no-editor placeholder),
     * accounting for the current size of all visible panels, toolbar, & status bar.
     * @return {number}
     */
    function _calcEditorHeight() {
        var availableHt = $(".content").height();

        _editorHolder.siblings().each(function (i, elem) {
            var $elem = $(elem);
            if ($elem.css("display") !== "none") {
                availableHt -= $elem.outerHeight();
            }
        });

        // Clip value to 0 (it could be negative if a panel wants more space than we have)
        return Math.max(availableHt, 0);
    }

    /**
     * Resize the editor. This must be called any time the contents of the editor area are swapped
     * or any time the editor area might change height. EditorManager takes care of calling this when
     * the Editor is swapped, and on window resize. But anyone who changes size/visiblity of editor
     * area siblings (toolbar, status bar, bottom panels) *must* manually call resizeEditor().
     *
     * @param {boolean=} skipRefresh For internal use, to avoid redundant refresh()es during window resize
     */
    function resizeEditor(skipRefresh) {
        if (!_editorHolder) {
            return;  // still too early during init
        }

        var editorAreaHt = _calcEditorHeight();
        _editorHolder.height(editorAreaHt);    // affects size of "not-editor" placeholder as well

        if (_currentEditor) {
            $(_currentEditor.getScrollerElement()).height(editorAreaHt);
            if (!skipRefresh) {
                _currentEditor.refresh(true);
            }
        }
    }

    /**
     * NJ's editor-resizing fix. Whenever the window resizes, we immediately adjust the editor's
     * height.
     */
    function _updateEditorDuringResize() {
        // skipRefresh=true since CodeMirror will call refresh() itself when it sees the resize event
        resizeEditor(true);
    }

    /**
     * @private
     * @param {?Editor} current
     */
    function _notifyActiveEditorChanged(current) {
        // Skip if the Editor that gained focus was already the most recently focused editor.
        // This may happen e.g. if the window loses then regains focus.
        if (_lastFocusedEditor === current) {
            return;
        }
        var previous = _lastFocusedEditor;
        _lastFocusedEditor = current;

        $(exports).triggerHandler("activeEditorChange", [current, previous]);
    }

    /**
     * @private
     */
    function _doShow(document) {
        // Show new editor
        _currentEditorsDocument = document;
        _currentEditor = document._masterEditor;

        _currentEditor.setVisible(true);
        _currentEditor.focus();

        // Window may have been resized since last time editor was visible, so kick it now
        resizeEditor();
    }

    /**
     * Make the given document's editor visible in the UI, hiding whatever was
     * visible before. Creates a new editor if none is assigned.
     * @param {!Document} document
     */
    function _showEditor(document) {
        // Hide whatever was visible before
        if (!_currentEditor) {
            $("#not-editor").css("display", "none");
        } else {
            _currentEditor.setVisible(false);
            _destroyEditorIfUnneeded(_currentEditorsDocument);
        }

        // Ensure a main editor exists for this document to show in the UI
        if (!document._masterEditor) {
            // Editor doesn't exist: populate a new Editor with the text
            _createFullEditorForDocument(document);
        }

        _doShow(document);
    }

    /** Hide the currently visible editor and show a placeholder UI in its place */
    function _showNoEditor() {
        if (_currentEditor) {
            _currentEditor.setVisible(false);
            _destroyEditorIfUnneeded(_currentEditorsDocument);

            _currentEditorsDocument = null;
            _currentEditor = null;

            $("#not-editor").css("display", "");

            // No other Editor is gaining focus, so in this one special case we must trigger event manually
            _notifyActiveEditorChanged(null);
        }
    }

    /** Handles changes to DocumentManager.getCurrentDocument() */
    function _onCurrentDocumentChange() {
        var doc = DocumentManager.getCurrentDocument(),
            container = _editorHolder.get(0);

        var perfTimerName = PerfUtils.markStart("EditorManager._onCurrentDocumentChange():\t" + (!doc || doc.file.fullPath));

        // Remove scroller-shadow from the current editor
        if (_currentEditor) {
            ViewUtils.removeScrollerShadow(container, _currentEditor);
        }

        // Update the UI to show the right editor (or nothing), and also dispose old editor if no
        // longer needed.
        if (doc) {
            _showEditor(doc);
            ViewUtils.addScrollerShadow(container, _currentEditor);
        } else {
            _showNoEditor();
        }

        PerfUtils.addMeasurement(perfTimerName);
    }

    /** Handles removals from DocumentManager's working set list */
    function _onWorkingSetRemove(event, removedFile) {
        // There's one case where an editor should be disposed even though the current document
        // didn't change: removing a document from the working set (via the "X" button). (This may
        // also cover the case where the document WAS current, if the editor-swap happens before the
        // removal from the working set.
        var doc = DocumentManager.getOpenDocumentForPath(removedFile.fullPath);
        if (doc) {
            _destroyEditorIfUnneeded(doc);
        }
        // else, file was listed in working set but never shown in the editor - ignore
    }

    function _onWorkingSetRemoveList(event, removedFiles) {
        removedFiles.forEach(function (removedFile) {
            _onWorkingSetRemove(event, removedFile);
        });
    }

    // Note: there are several paths that can lead to an editor getting destroyed
    //  - file was in working set, but not in current editor; then closed (via working set "X" button)
    //      --> handled by _onWorkingSetRemove()
    //  - file was in current editor, but not in working set; then navigated away from
    //      --> handled by _onCurrentDocumentChange()
    //  - file was in current editor, but not in working set; then closed (via File > Close) (and thus
    //    implicitly navigated away from)
    //      --> handled by _onCurrentDocumentChange()
    //  - file was in current editor AND in working set; then closed (via File > Close OR working set
    //    "X" button) (and thus implicitly navigated away from)
    //      --> handled by _onWorkingSetRemove() currently, but could be _onCurrentDocumentChange()
    //      just as easily (depends on the order of events coming from DocumentManager)

    /**
     * Designates the DOM node that will contain the currently active editor instance. EditorManager
     * will own the content of this DOM node.
     * @param {!jQueryObject} holder
     */
    function setEditorHolder(holder) {
        if (_currentEditor) {
            throw new Error("Cannot change editor area after an editor has already been created!");
        }

        _editorHolder = holder;

        resizeEditor();  // if no files open at startup, we won't get called back later to resize the "no-editor" placeholder
    }

    /**
     * Returns the currently focused inline widget.
     * @returns {?{widget:!InlineTextEditor, editor:!Editor}}
     */
    function getFocusedInlineWidget() {
        var result = null;

        if (_currentEditor) {
            _currentEditor.getInlineWidgets().forEach(function (widget) {
                if (widget instanceof InlineTextEditor) {
                    widget.editors.forEach(function (editor) {
                        if (editor.hasFocus()) {
                            result = { widget: widget, editor: editor };
                        }
                    });
                }
            });
        }

        return result;
    }

    function _getFocusedInlineEditor() {
        var focusedInline = getFocusedInlineWidget();
        if (focusedInline) {
            return focusedInline.editor;
        }

        return null;
    }

    /**
     * Returns the currently focused editor instance (full-sized OR inline editor).
     * This function is similar to getActiveEditor(), with one main difference: this
     * function will only return editors that currently have focus, whereas
     * getActiveEditor() will return the last visible editor that was given focus (but
     * may not currently have focus because, for example, a dialog with editable text
     * is open).
     * @returns {Editor}
     */
    function getFocusedEditor() {
        if (_currentEditor) {

            // See if any inlines have focus
            var focusedInline = _getFocusedInlineEditor();
            if (focusedInline) {
                return focusedInline;
            }

            // otherwise, see if full-sized editor has focus
            if (_currentEditor.hasFocus()) {
                return _currentEditor;
            }
        }

        return null;
    }

    /**
     * Returns the current active editor (full-sized OR inline editor). This editor may not
     * have focus at the moment, but it is visible and was the last editor that was given
     * focus. Returns null if no editors are active.
     * @see getFocusedEditor()
     * @returns {Editor}
     */
    function getActiveEditor() {
        return _lastFocusedEditor;
    }

    /**
     * Toggle Quick Edit command handler
     * @return {!Promise} A promise resolved with true if an inline editor
     *   is opened or false when closed. The promise is rejected if there
     *   is no current editor or an inline editor is not created.
     */
    function _toggleQuickEdit() {
        var result = new $.Deferred();

        if (_currentEditor) {
            var inlineWidget = null,
                focusedWidgetResult = getFocusedInlineWidget();

            if (focusedWidgetResult) {
                inlineWidget = focusedWidgetResult.widget;
            }

            if (inlineWidget) {
                // an inline widget's editor has focus, so close it
                PerfUtils.markStart(PerfUtils.INLINE_EDITOR_CLOSE);
                inlineWidget.close();
                PerfUtils.addMeasurement(PerfUtils.INLINE_EDITOR_CLOSE);

                // return a resolved promise to CommandManager
                result.resolve(false);
            } else {
                // main editor has focus, so create an inline editor
                _openInlineWidget(_currentEditor).done(function () {
                    result.resolve(true);
                }).fail(function () {
                    result.reject();
                });
            }
        } else {
            // Can not open an inline editor without a host editor
            result.reject();
        }

        return result.promise();
    }

    function _updateModeInfo(editor) {
        $modeInfo.text(StatusBar.getModeDisplayString(editor.getModeForDocument()));
    }

    function _updateFileInfo(editor) {
        $fileInfo.text(StringUtils.format(Strings.STATUSBAR_LINE_COUNT, editor.lineCount()));
    }

    function _updateIndentType() {
        var indentWithTabs = Editor.getUseTabChar();
        $indentType.text(indentWithTabs ? Strings.STATUSBAR_TAB_SIZE : Strings.STATUSBAR_SPACES);
        $indentType.attr("title", indentWithTabs ? Strings.STATUSBAR_INDENT_TOOLTIP_SPACES : Strings.STATUSBAR_INDENT_TOOLTIP_TABS);
        $indentWidthLabel.attr("title", indentWithTabs ? Strings.STATUSBAR_INDENT_SIZE_TOOLTIP_TABS : Strings.STATUSBAR_INDENT_SIZE_TOOLTIP_SPACES);
    }

    function _getIndentSize() {
        return Editor.getUseTabChar() ? Editor.getTabSize() : Editor.getIndentUnit();
    }

    function _updateIndentSize() {
        var size = _getIndentSize();
        $indentWidthLabel.text(size);
        $indentWidthInput.val(size);
    }

    function _toggleIndentType() {
        Editor.setUseTabChar(!Editor.getUseTabChar());
        _updateIndentType();
        _updateIndentSize();
    }

    function _updateCursorInfo(event, editor) {
        editor = editor || getFocusedEditor();

        // compute columns, account for tab size
        var cursor = editor.getCursorPos(true);

        $cursorInfo.text(StringUtils.format(Strings.STATUSBAR_CURSOR_POSITION, cursor.line + 1, cursor.ch + 1));
    }

    function _changeIndentWidth(value) {
        $indentWidthLabel.removeClass("hidden");
        $indentWidthInput.addClass("hidden");

        // remove all event handlers from the input field
        $indentWidthInput.off("blur keyup");

        // restore focus to the editor
        focusEditor();

        if (!value || isNaN(value)) {
            return;
        }

        if (Editor.getUseTabChar()) {
            Editor.setTabSize(Math.max(Math.min(value, 10), 1));
        } else {
            Editor.setIndentUnit(Math.max(Math.min(value, 10), 1));
        }

        // update indicator
        _updateIndentSize();

        // column position may change when tab size changes
        _updateCursorInfo();
    }

    function _onActiveEditorChange(event, current, previous) {
        if (previous) {
            $(previous).off("cursorActivity.statusbar");
            $(previous).off("change.statusbar");
        }

        if (!current) {
            StatusBar.hide();  // calls resizeEditor() if needed
        } else {
            StatusBar.show();  // calls resizeEditor() if needed

            $(current).on("cursorActivity.statusbar", _updateCursorInfo);
            $(current).on("change.statusbar", function () {
                // async update to keep typing speed smooth
                window.setTimeout(function () { _updateFileInfo(current); }, 0);
            });

            _updateCursorInfo(null, current);
            _updateModeInfo(current);
            _updateFileInfo(current);
            _updateIndentType();
            _updateIndentSize();
        }
    }

    function _onFileNameChange(event, oldName, newName) {

        // The current document file entry has already been updated.
        // We only need to update the editor mode to match the new file extension
        var editor = getCurrentFullEditor();

        if (editor && editor.document.file.fullPath === newName) {
            editor.setModeForDocument(
                EditorUtils.getModeFromFileExtension(editor.document.file.fullPath)
            );
        }
    }

    function _init() {
        StatusBar.init($(".main-view .content"));

        $modeInfo           = $("#status-mode");
        $cursorInfo         = $("#status-cursor");
        $fileInfo           = $("#status-file");
        $indentType         = $("#indent-type");
        $indentWidthLabel   = $("#indent-width-label");
        $indentWidthInput   = $("#indent-width-input");

        // indentation event handlers
        $indentType.on("click", _toggleIndentType);
        $indentWidthLabel
            .on("click", function () {
                // update the input value before displaying
                $indentWidthInput.val(_getIndentSize());

                $indentWidthLabel.addClass("hidden");
                $indentWidthInput.removeClass("hidden");
                $indentWidthInput.focus();

                $indentWidthInput
                    .on("blur", function () {
                        _changeIndentWidth($indentWidthInput.val());
                    })
                    .on("keyup", function (event) {
                        if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
                            $indentWidthInput.blur();
                        } else if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
                            _changeIndentWidth(false);
                        }
                    });
            });

        $indentWidthInput.focus(function () { $indentWidthInput.select(); });

        _onActiveEditorChange(null, getFocusedEditor(), null);
    }

    // Initialize: command handlers
    CommandManager.register(Strings.CMD_TOGGLE_QUICK_EDIT, Commands.TOGGLE_QUICK_EDIT, _toggleQuickEdit);

    // Initialize: register listeners
    $(DocumentManager).on("currentDocumentChange", _onCurrentDocumentChange);
    $(DocumentManager).on("workingSetRemove", _onWorkingSetRemove);
    $(DocumentManager).on("workingSetRemoveList", _onWorkingSetRemoveList);
    $(DocumentManager).on("fileNameChange", _onFileNameChange);

    // Add this as a capture handler so we're guaranteed to run it before the editor does its own
    // refresh on resize.
    window.addEventListener("resize", _updateEditorDuringResize, true);

    // Initialize: status bar focused listener
    $(exports).on("activeEditorChange", _onActiveEditorChange);

    AppInit.htmlReady(_init);

    // For unit tests and internal use only
    exports._init = _init;
    exports._openInlineWidget = _openInlineWidget;
    exports._notifyActiveEditorChanged = _notifyActiveEditorChanged;
    exports._createFullEditorForDocument = _createFullEditorForDocument;
    exports._destroyEditorIfUnneeded = _destroyEditorIfUnneeded;

    // Define public API
    exports.setEditorHolder = setEditorHolder;
    exports.getCurrentFullEditor = getCurrentFullEditor;
    exports.createInlineEditorForDocument = createInlineEditorForDocument;
    exports.focusEditor = focusEditor;
    exports.getFocusedEditor = getFocusedEditor;
    exports.getActiveEditor = getActiveEditor;
    exports.getFocusedInlineWidget = getFocusedInlineWidget;
    exports.resizeEditor = resizeEditor;
    exports.registerInlineEditProvider = registerInlineEditProvider;
    exports.getInlineEditors = getInlineEditors;
    exports.closeInlineWidget = closeInlineWidget;
    exports.mergeExtraKeys = mergeExtraKeys;
});


// Hide if an error occurs
.vjs-error .vjs-text-track-settings {
display: none;
}

.vjs-modal-dialog.vjs-text-track-settings {
  background-color: $primary-background-color;
  background-color: rgba($primary-background-color, 0.75);
  color: $primary-foreground-color;
  height: 70%;
}

// Layout divs
.vjs-text-track-settings .vjs-modal-dialog-content {
  display: table;
}

.vjs-text-track-settings .vjs-track-settings-colors,
.vjs-text-track-settings .vjs-track-settings-font,
.vjs-text-track-settings .vjs-track-settings-controls {
  display: table-cell;
}

.vjs-text-track-settings .vjs-track-settings-controls {
  text-align: right;
  vertical-align: bottom;
}

// code that will only run if CSS Grid is supported by the browser
@supports (display: grid) {
  .vjs-text-track-settings .vjs-modal-dialog-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr;
    // Flex and Grid for Firefox, IE, and Edge remove the bottom padding/margin in a container as size decreases
    // so we add bottom padding/margin to the last item in the grid instead of here
    // see https://stackoverflow.com/a/23754080
    padding: 20px 24px 0px 24px;
  }

  // see the comment for padding above
  .vjs-track-settings-controls .vjs-default-button {
    margin-bottom: 20px;
  }

  .vjs-text-track-settings .vjs-track-settings-controls {
    // make this take up both columns
    grid-column: 1 / -1;
  }

  // 1 column for small players
  .vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content ,
  .vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content,
  .vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content {
    grid-template-columns: 1fr;
  }

}

// Form elements
.vjs-text-track-settings select {
  font-size: inherit;
}

.vjs-track-setting > select {
  margin-right: 1em;
  margin-bottom: 0.5em;
}

.vjs-text-track-settings fieldset {
  margin: 10px;
  border: none;
}

.vjs-text-track-settings fieldset span {
  display: inline-block;
  padding: 0 .6em .8em;
}

// style the second select for text colors
.vjs-text-track-settings fieldset span > select {
  max-width: 7.3em;
}

.vjs-text-track-settings legend {
  color: $primary-foreground-color;
  font-weight: bold;
  font-size: 1.2em;
}

.vjs-text-track-settings .vjs-label {
  margin: 0 .5em .5em 0;
}

.vjs-track-settings-controls button:focus,
.vjs-track-settings-controls button:active {
  outline-style: solid;
  outline-width: medium;
  background-image: linear-gradient(0deg, $primary-foreground-color 88%, $secondary-background-color 100%);
}

.vjs-track-settings-controls button:hover {
  color: rgba(#2B333F, 0.75);
}

.vjs-track-settings-controls button {
  background-color: $primary-foreground-color;
  background-image: linear-gradient(-180deg, $primary-foreground-color 88%, $secondary-background-color 100%);
  color: #2B333F;
  cursor: pointer;
  border-radius: 2px;
}

.vjs-track-settings-controls .vjs-default-button {
  margin-right: 1em;
}


<!DOCTYPE html><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>MouseTimeDisplay</title><link href="https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700" rel="stylesheet" type="text/css"/> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/atelier-forest.light.min.css"/><link rel="stylesheet" type="text/css" href="css/api-docs.css"/><link rel="canonical" href="https://docs.videojs.com/MouseTimeDisplay.html">

This documentation is for an outdated version of Video.js. See documentation for the current release.

API Index

MouseTimeDisplay Methods

Class Methods

createEl

Inherited Methods from Component

$

$$

addChild

addClass

buildCSSClass

children

clearInterval

clearTimeout

contentEl

dimensions

dispose

el

enableTouchActivity

extend

getChild

getChildById

getComponent

hasClass

height

hide

id

initChildren

name

off

on

one

options

player

ready

registerComponent

removeChild

removeClass

setInterval

setTimeout

show

toggleClass

trigger

triggerReady

width

MouseTimeDisplay

The Mouse Time Display component shows the time you will seek to
when hovering over the progress bar

DEFINED IN: mouse-time-display.js line number: 10

EXTENDS: component.js

Constructor

MouseTimeDisplay( player,[options] )

Parameters

name	Type	Required	Description

player	Player|Object

options	Object

Methods

createEl()

Create the component's DOM element

Defined in https://github.com/videojs/video.js/blob/master/src/js/control-bar/progress-control/mouse-time-display.js line number: 31

[back to top]

$( selector, [context] )

Finds a single DOM element matching selector within the component's
contentEl or another custom context.

Parameters

name	Type	Required	Description

selector	String	yes	A valid CSS selector, which will be passed to querySelector.

context	Element|String	no	A DOM element within which to query. Can also be a selector
        string in which case the first matching element will be used
        as context. If missing (or no element matches selector), falls
        back to document.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 841

[back to top]

$$( selector, [context] )

Finds a all DOM elements matching selector within the component's
contentEl or another custom context.

Parameters

name	Type	Required	Description

selector	String	yes	A valid CSS selector, which will be passed to querySelectorAll.

context	Element|String	no	A DOM element within which to query. Can also be a selector
        string in which case the first matching element will be used
        as context. If missing (or no element matches selector), falls
        back to document.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 861

[back to top]

addChild( child, [options] )

Adds a child component inside this component

    myComponent.el();
    // ->

    myComponent.children();
    // [empty array]

    var myButton = myComponent.addChild('MyButton');
    // ->

myButton

    // -> myButton === myComponent.children()[0];

Pass in options for child constructors and options for children of the child

    var myButton = myComponent.addChild('MyButton', {
      text: 'Press Me',
      buttonChildExample: {
        buttonChildOption: true
      }
    });

Parameters

name	Type	Required	Description

child	String|Component	yes	The class name or instance of a child to add

options	Object	no	Options, including options to be passed to children of the child.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 315

[back to top]

addClass( classToAdd )

Add a CSS class name to the component's element

Parameters

name	Type	Required	Description

classToAdd	String	yes	Classname to add

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 892

[back to top]

buildCSSClass()

Allows sub components to stack CSS class names

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 593

[back to top]

children()

Get an array of all child components

    var kids = myComponent.children();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 282

[back to top]

clearInterval( intervalId )

Clears an interval and removes the associated dispose listener

Parameters

name	Type	Required	Description

intervalId	Number	yes	The id of the interval to clear

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1295

[back to top]

clearTimeout( timeoutId )

Clears a timeout and removes the associated dispose listener

Parameters

name	Type	Required	Description

timeoutId	Number	yes	The id of the timeout to clear

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1252

[back to top]

contentEl()

Return the component's DOM element where children are inserted.
Will either be the same as el() or a new element defined in createEl().

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 245

[back to top]

dimensions( width, height )

Set both width and height at the same time

Parameters

name	Type	Required	Description

width	Number|String	yes	Width of player

height	Number|String	yes	Height of player

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1015

[back to top]

dispose()

Dispose of the component and all child components

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 101

[back to top]

el()

Get the component's DOM element

    var domEl = myComponent.el();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 195

[back to top]

enableTouchActivity()

Report user touch activity when touch events occur
User activity is used to determine when controls should show/hide. It's
relatively simple when it comes to mouse events, because any mouse event
should show the controls. So we capture mouse events that bubble up to the
player and report activity when that happens.
With touch events it isn't as easy. We can't rely on touch events at the
player level, because a tap (touchstart + touchend) on the video itself on
mobile devices is meant to turn controls off (and on). User activity is
checked asynchronously, so what could happen is a tap event on the video
turns the controls off, then the touchend event bubbles up to the player,
which if it reported user activity, would turn the controls right back on.
(We also don't want to completely block touch events from bubbling up)
Also a touchmove, touch+hold, and anything other than a tap is not supposed
to turn the controls back on on a mobile device.
Here we're setting the default component behavior to report user activity
whenever touch events happen, and this can be turned off by components that
want touch events to act differently.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1174

[back to top]

static extend( props ) (deprecated)

Sets up the constructor using the supplied init method
or uses the init of the parent object

Parameters

name	Type	Required	Description

props	Object	yes	An object of properties

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1350

[back to top]

getChild()

Returns a child component with the provided name

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 305

[back to top]

getChildById()

Returns a child component with the provided ID

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 295

[back to top]

static getComponent( name )

Gets a component by name

Parameters

name	Type	Required	Description

name	String	yes	Name of the component to get

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1331

[back to top]

hasClass( classToCheck )

Check if a component's element has a CSS class name

Parameters

name	Type	Required	Description

classToCheck	String	yes	Classname to check

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 881

[back to top]

height( num, [skipListeners] )

Get or set the height of the component (CSS values)
Setting the video tag dimension values only works with values in pixels.
Percent values will not work.
Some percents can be used, but width()/height() will return the number + %,
not the actual computed width/height.

Parameters

name	Type	Required	Description

num	Number|String	yes	New component height

skipListeners	Boolean	no	Skip the resize event trigger

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 998

[back to top]

hide()

Hide the component element if currently showing

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 944

[back to top]

id()

Get the component's ID

    var id = myComponent.id();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 256

[back to top]

initChildren()

Add and initialize default child components from options

    // when an instance of MyComponent is created, all children in options
    // will be added to the instance by their name strings and options
    MyComponent.prototype.options_ = {
      children: [
        'myChildComponent'
      ],
      myChildComponent: {
        myChildOption: true
      }
    };

    // Or when creating the component
    var myComp = new MyComponent(player, {
      children: [
        'myChildComponent'
      ],
      myChildComponent: {
        myChildOption: true
      }
    });

The children option can also be an array of
child options objects (that also include a 'name' key).
This can be used if you have two child components of the
same type that need different options.

    var myComp = new MyComponent(player, {
      children: [
        'button',
        {
          name: 'button',
          someOtherOption: true
        },
        {
          name: 'button',
          someOtherOption: false
        }
      ]
    });

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 455

[back to top]

name()

Get the component's name. The name is often used to reference the component.

    var name = myComponent.name();

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 269

[back to top]

off( first, second, [third] )

Remove an event listener from this component's element

    myComponent.off('eventType', myFunc);

If myFunc is excluded, ALL listeners for the event type will be removed.
If eventType is excluded, ALL listeners will be removed from the component.
Alternatively you can use off to remove listeners that were added to other
elements or components using myComponent.on(otherComponent....
In this case both the event type and listener function are REQUIRED.

    myComponent.off(otherElement, 'eventType', myFunc);
    myComponent.off(otherComponent, 'eventType', myFunc);

Parameters

name	Type	Required	Description

first	String|Component	yes	The event type or other component

second	function|String	yes	The listener function or event type

third	function	no	The listener for other component

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 680

[back to top]

on( first, second, third )

Add an event listener to this component's element

    var myFunc = function(){
      var myComponent = this;
      // Do something when the event is fired
    };

    myComponent.on('eventType', myFunc);

The context of myFunc will be myComponent unless previously bound.
Alternatively, you can add a listener to another element or component.

    myComponent.on(otherElement, 'eventName', myFunc);
    myComponent.on(otherComponent, 'eventName', myFunc);

The benefit of using this over VjsEvents.on(otherElement, 'eventName', myFunc)
and otherComponent.on('eventName', myFunc) is that this way the listeners
will be automatically cleaned up when either component is disposed.
It will also bind myComponent as the context of myFunc.
NOTE: When using this on elements in the page other than window
and document (both permanent), if you remove the element from the DOM
you need to call myComponent.trigger(el, 'dispose') on it to clean up
references to it and allow the browser to garbage collect it.

Parameters

name	Type	Required	Description

first	String|Component	yes	The event type or other component

second	function|String	yes	The event handler or event type

third	function	yes	The event handler

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 605

[back to top]

one( first, second, [third] )

Add an event listener to be triggered only once and then removed

    myComponent.one('eventName', myFunc);

Alternatively you can add a listener to another element or component
that will be triggered only once.

    myComponent.one(otherElement, 'eventName', myFunc);
    myComponent.one(otherComponent, 'eventName', myFunc);

Parameters

name	Type	Required	Description

first	String|Component	yes	The event type or other component

second	function|String	yes	The listener function or event type

third	function	no	The listener function for other component

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 728

[back to top]

options( obj )

Deep merge of options objects
Whenever a property is an object on both options objects
the two properties will be merged using mergeOptions.

    Parent.prototype.options_ = {
      optionSet: {
        'childOne': { 'foo': 'bar', 'asdf': 'fdsa' },
        'childTwo': {},
        'childThree': {}
      }
    }
    newOptions = {
      optionSet: {
        'childOne': { 'foo': 'baz', 'abc': '123' }
        'childTwo': null,
        'childFour': {}
      }
    }

    this.options(newOptions);

RESULT

    {
      optionSet: {
        'childOne': { 'foo': 'baz', 'asdf': 'fdsa', 'abc': '123' },
        'childTwo': null, // Disabled. Won't be initialized.
        'childThree': {},
        'childFour': {}
      }
    }

Parameters

name	Type	Required	Description

obj	Object	yes	Object of new option values

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 145

[back to top]

player()

Return the component's player

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 135

[back to top]

ready( fn, sync )

Bind a listener to the component's ready state.
Different from event listeners in that if the ready event has already happened
it will trigger the function immediately.

Parameters

name	Type	Required	Description

fn	function	yes	Ready listener

sync	Boolean	yes	Exec the listener synchronously if component is ready

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 787

[back to top]

static registerComponent( name, comp )

Registers a component

Parameters

name	Type	Required	Description

name	String	yes	Name of the component to register

comp	Object	yes	The component to register

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1314

[back to top]

removeChild( component )

Remove a child component from this component's list of children, and the
child component's element from this component's element

Parameters

name	Type	Required	Description

component	Component	yes	Component to remove

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 415

[back to top]

removeClass( classToRemove )

Remove a CSS class name from the component's element

Parameters

name	Type	Required	Description

classToRemove	String	yes	Classname to remove

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 904

[back to top]

setInterval( fn, interval )

Creates an interval and sets up disposal automatically.

Parameters

name	Type	Required	Description

fn	function	yes	The function to run every N seconds.

interval	Number	yes	Number of ms to delay before executing specified function.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1271

[back to top]

setTimeout( fn, timeout )

Creates timeout and sets up disposal automatically.

Parameters

name	Type	Required	Description

fn	function	yes	The function to run after the timeout.

timeout	Number	yes	Number of ms to delay before executing specified function.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 1227

[back to top]

show()

Show the component element if hidden

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 933

[back to top]

toggleClass( classToToggle, [predicate] )

Add or remove a CSS class name from the component's element

Parameters

name	Type	Required	Description

classToToggle	String

predicate	Boolean|function	no	Can be a function that returns a Boolean. If true, the class
        will be added; if false, the class will be removed. If not
        given, the class will be added if not present and vice versa.

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 916

[back to top]

trigger( event, [hash] )

Trigger an event on an element

    myComponent.trigger('eventName');
    myComponent.trigger({'type':'eventName'});
    myComponent.trigger('eventName', {data: 'some data'});
    myComponent.trigger({'type':'eventName'}, {data: 'some data'});

Parameters

name	Type	Required	Description

event	Event|Object|String	yes	A string (the type) or an event object with a type attribute

hash	Object	no	data hash to pass along with the event

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 768

[back to top]

triggerReady()

Trigger the ready listeners

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 814

[back to top]

width( num, skipListeners )

Set or get the width of the component (CSS values)
Setting the video tag dimension values only works with values in pixels.
Percent values will not work.
Some percents can be used, but width()/height() will return the number + %,
not the actual computed width/height.

Parameters

name	Type	Required	Description

num	Number|String	yes	Optional width number

skipListeners	Boolean	yes	Skip the 'resize' event trigger

Defined in https://github.com/videojs/video.js/blob/master/src/js/component.js line number: 981

[back to top]


/* eslint-env qunit */
import document from 'global/document';
import keycode from 'keycode';
import sinon from 'sinon';
import TestHelpers from './test-helpers';
import FullscreenApi from '../../src/js/fullscreen-api.js';

QUnit.module('Player: User Actions: Click', {

  beforeEach() {
    this.clock = sinon.useFakeTimers();
    this.player = TestHelpers.makePlayer({controls: true});
  },

  afterEach() {
    this.player.dispose();
    this.clock.restore();
  }
});

QUnit.test('by default, click toggles play', function(assert) {
  let paused = true;

  this.player.paused = () => paused;
  this.player.play = sinon.spy();
  this.player.pause = sinon.spy();

  this.player.handleTechClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.play.callCount, 1, 'has called play');
  assert.strictEqual(this.player.pause.callCount, 0, 'has not called pause');

  paused = false;
  this.player.handleTechClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.play.callCount, 1, 'has called play, previously');
  assert.strictEqual(this.player.pause.callCount, 1, 'has called pause');
});

QUnit.test('when controls are disabled, click does nothing', function(assert) {
  let paused = true;

  this.player.controls(false);

  this.player.paused = () => paused;
  this.player.play = sinon.spy();
  this.player.pause = sinon.spy();

  this.player.handleTechClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.play.callCount, 0, 'has not called play');
  assert.strictEqual(this.player.pause.callCount, 0, 'has not called pause');

  paused = false;
  this.player.handleTechClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.play.callCount, 0, 'has not called play, previously');
  assert.strictEqual(this.player.pause.callCount, 0, 'has not called pause');
});

QUnit.test('when userActions.click is false, click does nothing', function(assert) {
  let paused = true;

  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      click: false
    }
  });

  this.player.paused = () => paused;
  this.player.play = sinon.spy();
  this.player.pause = sinon.spy();

  this.player.handleTechClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.play.callCount, 0, 'has not called play');
  assert.strictEqual(this.player.pause.callCount, 0, 'has not called pause');

  paused = false;
  this.player.handleTechClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.play.callCount, 0, 'has not called play, previously');
  assert.strictEqual(this.player.pause.callCount, 0, 'has not called pause');
});

QUnit.test('when userActions.click is a function, that function is called instead of toggling play', function(assert) {
  let paused = true;
  const clickSpy = sinon.spy();

  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      click: clickSpy
    }
  });

  this.player.paused = () => paused;
  this.player.play = sinon.spy();
  this.player.pause = sinon.spy();

  let event = {target: this.player.tech_.el_};

  this.player.handleTechClick_(event);

  assert.strictEqual(this.player.play.callCount, 0, 'has not called play');
  assert.strictEqual(this.player.pause.callCount, 0, 'has not called pause');
  assert.strictEqual(clickSpy.callCount, 1, 'has called the click handler');
  assert.strictEqual(clickSpy.getCall(0).args[0], event, 'has passed the event to the handler');

  paused = false;
  event = {target: this.player.tech_.el_};
  this.player.handleTechClick_(event);

  assert.strictEqual(this.player.play.callCount, 0, 'has not called play, previously');
  assert.strictEqual(this.player.pause.callCount, 0, 'has not called pause');
  assert.strictEqual(clickSpy.callCount, 2, 'has called the click handler');
  assert.strictEqual(clickSpy.getCall(1).args[0], event, 'has passed the event to the handler');
});

QUnit.module('Player: User Actions: Double Click', {

  beforeEach() {
    this.clock = sinon.useFakeTimers();
    this.player = TestHelpers.makePlayer({controls: true});
  },

  afterEach() {
    this.player.dispose();
    this.clock.restore();
  }
});

QUnit.test('by default, double-click opens fullscreen', function(assert) {
  let fullscreen = false;

  this.player.isFullscreen = () => fullscreen;
  this.player.requestFullscreen = sinon.spy();
  this.player.exitFullscreen = sinon.spy();

  this.player.handleTechDoubleClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen once');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');

  fullscreen = true;
  this.player.handleTechDoubleClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen once');
  assert.strictEqual(this.player.exitFullscreen.callCount, 1, 'has exited fullscreen');
});

QUnit.test('when controls are disabled, double-click does nothing', function(assert) {
  let fullscreen = false;

  this.player.controls(false);

  this.player.isFullscreen = () => fullscreen;
  this.player.requestFullscreen = sinon.spy();
  this.player.exitFullscreen = sinon.spy();

  this.player.handleTechDoubleClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');

  fullscreen = true;
  this.player.handleTechDoubleClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
});

QUnit.test('when userActions.doubleClick is false, double-click does nothing', function(assert) {
  let fullscreen = false;

  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      doubleClick: false
    }
  });

  this.player.isFullscreen = () => fullscreen;
  this.player.requestFullscreen = sinon.spy();
  this.player.exitFullscreen = sinon.spy();

  this.player.handleTechDoubleClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');

  fullscreen = true;
  this.player.handleTechDoubleClick_({target: this.player.tech_.el_});

  assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
});

QUnit.test('when userActions.doubleClick is a function, that function is called instead of going fullscreen', function(assert) {
  let fullscreen = false;

  const doubleClickSpy = sinon.spy();

  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      doubleClick: doubleClickSpy
    }
  });

  this.player.isFullscreen = () => fullscreen;
  this.player.requestFullscreen = sinon.spy();
  this.player.exitFullscreen = sinon.spy();

  let event = {target: this.player.tech_.el_};

  this.player.handleTechDoubleClick_(event);

  assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
  assert.strictEqual(doubleClickSpy.callCount, 1, 'has called the doubleClick handler');
  assert.strictEqual(doubleClickSpy.getCall(0).args[0], event, 'has passed the event to the handler');

  fullscreen = true;
  event = {target: this.player.tech_.el_};
  this.player.handleTechDoubleClick_(event);

  assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
  assert.strictEqual(doubleClickSpy.callCount, 2, 'has called the doubleClick handler');
  assert.strictEqual(doubleClickSpy.getCall(1).args[0], event, 'has passed the event to the handler');
});

QUnit.module('Player: User Actions: Hotkeys', {

  beforeEach() {
    this.clock = sinon.useFakeTimers();
    this.player = TestHelpers.makePlayer();
  },

  afterEach() {
    this.player.dispose();
    this.clock.restore();
  }
});

const mockKeyDownEvent = (key) => {
  return {
    preventDefault() {},
    stopPropagation() {},
    type: 'keydown',
    which: keycode.codes[key]
  };
};

const defaultKeyTests = {
  fullscreen(player, assert, positive) {
    let fullscreen;

    if (document[FullscreenApi.fullscreenEnabled] === false) {
      assert.ok(true, 'skipped fullscreen test because not supported');
      assert.ok(true, 'skipped fullscreen test because not supported');
      assert.ok(true, 'skipped fullscreen test because not supported');
      assert.ok(true, 'skipped fullscreen test because not supported');
      return;
    }

    player.isFullscreen = () => fullscreen;
    player.requestFullscreen = sinon.spy();
    player.exitFullscreen = sinon.spy();

    fullscreen = false;
    player.handleKeyDown(mockKeyDownEvent('f'));

    if (positive) {
      assert.strictEqual(player.requestFullscreen.callCount, 1, 'has gone fullscreen');
      assert.strictEqual(player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
    } else {
      assert.strictEqual(player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
      assert.strictEqual(player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
    }

    fullscreen = true;
    player.handleKeyDown(mockKeyDownEvent('f'));

    if (positive) {
      assert.strictEqual(player.requestFullscreen.callCount, 1, 'has gone fullscreen');
      assert.strictEqual(player.exitFullscreen.callCount, 1, 'has exited fullscreen');
    } else {
      assert.strictEqual(player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
      assert.strictEqual(player.exitFullscreen.callCount, 0, 'has not exited fullscreen');
    }
  },
  mute(player, assert, positive) {
    let muted = false;

    player.muted = sinon.spy((val) => {
      if (val !== undefined) {
        muted = val;
      }
      return muted;
    });

    player.handleKeyDown(mockKeyDownEvent('m'));

    if (positive) {
      assert.strictEqual(player.muted.callCount, 2, 'muted was called twice (get and set)');
      assert.strictEqual(player.muted.lastCall.args[0], true, 'most recent call was to mute');
    } else {
      assert.strictEqual(player.muted.callCount, 0, 'muted was not called');
    }

    player.handleKeyDown(mockKeyDownEvent('m'));

    if (positive) {
      assert.strictEqual(player.muted.callCount, 4, 'muted was called twice (get and set)');
      assert.strictEqual(player.muted.lastCall.args[0], false, 'most recent call was to unmute');
    } else {
      assert.strictEqual(player.muted.callCount, 0, 'muted was not called');
    }
  },
  playPause(player, assert, positive) {
    let paused;

    player.paused = () => paused;
    player.pause = sinon.spy();
    player.play = sinon.spy();

    paused = true;
    player.handleKeyDown(mockKeyDownEvent('k'));

    if (positive) {
      assert.strictEqual(player.pause.callCount, 0, 'has not paused');
      assert.strictEqual(player.play.callCount, 1, 'has played');
    } else {
      assert.strictEqual(player.pause.callCount, 0, 'has not paused');
      assert.strictEqual(player.play.callCount, 0, 'has not played');
    }

    paused = false;
    player.handleKeyDown(mockKeyDownEvent('k'));

    if (positive) {
      assert.strictEqual(player.pause.callCount, 1, 'has paused');
      assert.strictEqual(player.play.callCount, 1, 'has played');
    } else {
      assert.strictEqual(player.pause.callCount, 0, 'has not paused');
      assert.strictEqual(player.play.callCount, 0, 'has not played');
    }

    paused = true;
    player.handleKeyDown(mockKeyDownEvent('space'));

    if (positive) {
      assert.strictEqual(player.pause.callCount, 1, 'has paused');
      assert.strictEqual(player.play.callCount, 2, 'has played twice');
    } else {
      assert.strictEqual(player.pause.callCount, 0, 'has not paused');
      assert.strictEqual(player.play.callCount, 0, 'has not played');
    }

    paused = false;
    player.handleKeyDown(mockKeyDownEvent('space'));

    if (positive) {
      assert.strictEqual(player.pause.callCount, 2, 'has paused twice');
      assert.strictEqual(player.play.callCount, 2, 'has played twice');
    } else {
      assert.strictEqual(player.pause.callCount, 0, 'has not paused');
      assert.strictEqual(player.play.callCount, 0, 'has not played');
    }
  }
};

QUnit.test('by default, hotkeys are disabled', function(assert) {
  assert.expect(14);
  defaultKeyTests.fullscreen(this.player, assert, false);
  defaultKeyTests.mute(this.player, assert, false);
  defaultKeyTests.playPause(this.player, assert, false);
});

QUnit.test('when userActions.hotkeys is true, hotkeys are enabled', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: true
    }
  });

  assert.expect(16);
  defaultKeyTests.fullscreen(this.player, assert, true);
  defaultKeyTests.mute(this.player, assert, true);
  defaultKeyTests.playPause(this.player, assert, true);
});

QUnit.test('when userActions.hotkeys is an object, hotkeys are enabled', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: {}
    }
  });

  assert.expect(16);
  defaultKeyTests.fullscreen(this.player, assert, true);
  defaultKeyTests.mute(this.player, assert, true);
  defaultKeyTests.playPause(this.player, assert, true);
});

QUnit.test('when userActions.hotkeys.fullscreenKey can be a function', function(assert) {
  if (document[FullscreenApi.fullscreenEnabled] === false) {
    assert.expect(0);
    return;
  }

  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: {
        fullscreenKey: sinon.spy((e) => keycode.isEventKey(e, 'x'))
      }
    }
  });

  let fullscreen;

  this.player.isFullscreen = () => fullscreen;
  this.player.requestFullscreen = sinon.spy();
  this.player.exitFullscreen = sinon.spy();

  fullscreen = false;
  this.player.handleKeyDown(mockKeyDownEvent('f'));

  assert.strictEqual(this.player.requestFullscreen.callCount, 0, 'has not gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');

  this.player.handleKeyDown(mockKeyDownEvent('x'));

  assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 0, 'has not exited fullscreen');

  fullscreen = true;
  this.player.handleKeyDown(mockKeyDownEvent('x'));

  assert.strictEqual(this.player.requestFullscreen.callCount, 1, 'has gone fullscreen');
  assert.strictEqual(this.player.exitFullscreen.callCount, 1, 'has exited fullscreen');
});

QUnit.test('when userActions.hotkeys.muteKey can be a function', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: {
        muteKey: sinon.spy((e) => keycode.isEventKey(e, 'x'))
      }
    }
  });

  let muted = false;

  this.player.muted = sinon.spy((val) => {
    if (val !== undefined) {
      muted = val;
    }
    return muted;
  });

  this.player.handleKeyDown(mockKeyDownEvent('m'));

  assert.strictEqual(this.player.muted.callCount, 0, 'muted was not called');

  this.player.handleKeyDown(mockKeyDownEvent('x'));

  assert.strictEqual(this.player.muted.callCount, 2, 'muted was called twice (get and set)');
  assert.strictEqual(this.player.muted.lastCall.args[0], true, 'most recent call was to mute');

  this.player.handleKeyDown(mockKeyDownEvent('x'));

  assert.strictEqual(this.player.muted.callCount, 4, 'muted was called twice (get and set)');
  assert.strictEqual(this.player.muted.lastCall.args[0], false, 'most recent call was to unmute');
});

QUnit.test('when userActions.hotkeys.playPauseKey can be a function', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: {
        playPauseKey: sinon.spy((e) => keycode.isEventKey(e, 'x'))
      }
    }
  });

  let paused;

  this.player.paused = () => paused;
  this.player.pause = sinon.spy();
  this.player.play = sinon.spy();

  paused = true;
  this.player.handleKeyDown(mockKeyDownEvent('k'));
  this.player.handleKeyDown(mockKeyDownEvent('space'));

  assert.strictEqual(this.player.pause.callCount, 0, 'has not paused');
  assert.strictEqual(this.player.play.callCount, 0, 'has not played');

  this.player.handleKeyDown(mockKeyDownEvent('x'));

  assert.strictEqual(this.player.pause.callCount, 0, 'has not paused');
  assert.strictEqual(this.player.play.callCount, 1, 'has played');

  paused = false;
  this.player.handleKeyDown(mockKeyDownEvent('x'));

  assert.strictEqual(this.player.pause.callCount, 1, 'has paused');
  assert.strictEqual(this.player.play.callCount, 1, 'has played');
});

QUnit.test('hotkeys are ignored when focus is in a contenteditable element', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: true
    }
  });

  const div = document.createElement('div');

  div.contentEditable = 'true';
  this.player.el_.appendChild(div);
  div.focus();

  assert.expect(14);
  defaultKeyTests.fullscreen(this.player, assert, false);
  defaultKeyTests.mute(this.player, assert, false);
  defaultKeyTests.playPause(this.player, assert, false);
});

QUnit.test('hotkeys are ignored when focus is in a textarea', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: true
    }
  });

  const textarea = document.createElement('textarea');

  this.player.el_.appendChild(textarea);
  textarea.focus();

  assert.expect(14);
  defaultKeyTests.fullscreen(this.player, assert, false);
  defaultKeyTests.mute(this.player, assert, false);
  defaultKeyTests.playPause(this.player, assert, false);
});

QUnit.test('hotkeys are ignored when focus is in a text input', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: true
    }
  });

  const input = document.createElement('input');

  input.type = 'text';
  this.player.el_.appendChild(input);
  input.focus();

  assert.expect(14);
  defaultKeyTests.fullscreen(this.player, assert, false);
  defaultKeyTests.mute(this.player, assert, false);
  defaultKeyTests.playPause(this.player, assert, false);
});

QUnit.test('hotkeys are NOT ignored when focus is on a button element', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: true
    }
  });

  const button = document.createElement('button');

  this.player.el_.appendChild(button);
  button.focus();

  assert.expect(16);
  defaultKeyTests.fullscreen(this.player, assert, true);
  defaultKeyTests.mute(this.player, assert, true);
  defaultKeyTests.playPause(this.player, assert, true);
});

QUnit.test('hotkeys are NOT ignored when focus is on a button input', function(assert) {
  this.player.dispose();
  this.player = TestHelpers.makePlayer({
    controls: true,
    userActions: {
      hotkeys: true
    }
  });

  const input = document.createElement('input');

  input.type = 'button';
  this.player.el_.appendChild(input);
  input.focus();

  assert.expect(16);
  defaultKeyTests.fullscreen(this.player, assert, true);
  defaultKeyTests.mute(this.player, assert, true);
  defaultKeyTests.playPause(this.player, assert, true);
});


/* eslint-env qunit */
import TestHelpers from './test-helpers';

QUnit.module('Player: loadMedia/getMedia', {

  beforeEach() {
    this.player = TestHelpers.makePlayer({});
  },

  afterEach() {
    this.player.dispose();
  }
});

QUnit.test('loadMedia sets source from a string', function(assert) {
  this.player.loadMedia({
    src: 'foo.mp4'
  });

  assert.strictEqual(this.player.currentSrc(), 'foo.mp4', 'currentSrc was correct');
});

QUnit.test('loadMedia sets source from an object', function(assert) {
  this.player.loadMedia({
    src: {
      src: 'foo.mp4',
      type: 'video/mp4'
    }
  });

  assert.strictEqual(this.player.currentSrc(), 'foo.mp4', 'currentSrc was correct');
});

QUnit.test('loadMedia sets source from an array', function(assert) {
  const sources = [{
    src: 'foo.mp4',
    type: 'video/mp4'
  }, {
    src: 'foo.webm',
    type: 'video/webm'
  }];

  this.player.loadMedia({
    src: sources
  });

  assert.strictEqual(this.player.currentSrc(), sources[0].src, 'currentSrc was correct');
  assert.deepEqual(this.player.currentSource(), sources[0], 'currentSource was correct');
  assert.deepEqual(this.player.currentSources(), sources, 'currentSources were correct');
});

QUnit.test('loadMedia sets poster and backfills artwork', function(assert) {
  this.player.loadMedia({
    poster: 'foo.jpg'
  });

  assert.strictEqual(this.player.poster(), 'foo.jpg', 'poster was correct');
});

QUnit.test('loadMedia sets artwork via poster', function(assert) {
  this.player.loadMedia({
    poster: 'foo.jpg'
  });

  const {artwork} = this.player.getMedia();

  assert.deepEqual(artwork, [{
    src: 'foo.jpg',
    type: 'image/jpeg'
  }], 'the artwork was set to match the poster');
});

QUnit.test('loadMedia sets artwork and poster independently', function(assert) {
  this.player.loadMedia({
    poster: 'foo.jpg',
    artwork: [{
      src: 'bar.png',
      type: 'image/png'
    }]
  });

  assert.strictEqual(this.player.poster(), 'foo.jpg', 'poster was correct');
  assert.deepEqual(this.player.getMedia().artwork, [{
    src: 'bar.png',
    type: 'image/png'
  }], 'the artwork was provided, so does not match poster');
});

QUnit.test('loadMedia creates text tracks', function(assert) {
  this.player.loadMedia({
    textTracks: [{
      kind: 'captions',
      src: 'foo.vtt',
      language: 'en',
      label: 'English'
    }]
  });

  const rtt = this.player.remoteTextTracks()[0];

  assert.ok(Boolean(rtt), 'the track exists');
  assert.strictEqual(rtt.kind, 'captions', 'the kind is correct');
  assert.strictEqual(rtt.src, 'foo.vtt', 'the src is correct');
  assert.strictEqual(rtt.language, 'en', 'the language is correct');
  assert.strictEqual(rtt.label, 'English', 'the label is correct');
});

QUnit.test('getMedia returns a clone of the media object', function(assert) {
  const original = {
    arbitrary: true,
    src: 'foo.mp4',
    poster: 'foo.gif',
    textTracks: [{
      kind: 'captions',
      src: 'foo.vtt',
      language: 'en',
      label: 'English'
    }]
  };

  this.player.loadMedia(original);

  const result = this.player.getMedia();

  assert.notStrictEqual(result, original, 'a new object is returned');
  assert.deepEqual(result, {
    arbitrary: true,
    artwork: [{
      src: 'foo.gif',
      type: 'image/gif'
    }],
    src: 'foo.mp4',
    poster: 'foo.gif',
    textTracks: [{
      kind: 'captions',
      src: 'foo.vtt',
      language: 'en',
      label: 'English'
    }]
  }, 'the object has the expected structure');
});

QUnit.test('getMedia returns a new media object when no media has been loaded', function(assert) {

  this.player.poster = () => 'foo.gif';
  this.player.currentSources = () => [{src: 'foo.mp4', type: 'video/mp4'}];
  this.player.remoteTextTracks = () => [{
    kind: 'captions',
    src: 'foo.vtt',
    language: 'en',
    label: 'English'
  }, {
    kind: 'subtitles',
    src: 'bar.vtt',
    language: 'de',
    label: 'German'
  }];

  const result = this.player.getMedia();

  assert.deepEqual(result, {
    artwork: [{
      src: 'foo.gif',
      type: 'image/gif'
    }],
    src: [{
      src: 'foo.mp4',
      type: 'video/mp4'
    }],
    poster: 'foo.gif',
    textTracks: [{
      kind: 'captions',
      src: 'foo.vtt',
      language: 'en',
      label: 'English'
    }, {
      kind: 'subtitles',
      src: 'bar.vtt',
      language: 'de',
      label: 'German'
    }]
  }, 'the object has the expected structure');
});

// This only tests the relevant aspect of the reset function. The rest of its
// effects are tested in player.test.js
QUnit.test('reset discards the media object', function(assert) {
  this.player.loadMedia({
    poster: 'foo.jpg',
    src: 'foo.mp4',
    textTracks: [{src: 'foo.vtt'}]
  });

  this.player.reset();

  // TODO: There is a bug with player.reset() where it does not clear internal
  // cachces completely. Remove this when that's fixed.
  this.player.cache_.sources = [];

  assert.deepEqual(this.player.getMedia(), {src: [], textTracks: []}, 'any empty media object is returned');
});


// Hide if an error occurs
.vjs-error .vjs-loading-spinner {
display: none;
}

.vjs-loading-spinner {
  display: none;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  opacity: 0.85;

  // Need to fix centered page layouts
  text-align: left;

  border: .6em solid rgba($primary-background-color, $primary-background-transparency);
  // border: 6px solid rgba(43, 51, 63, 0.5);

  box-sizing: border-box;
  background-clip: padding-box;
  width: 5em;
  height: 5em;
  border-radius: 50%;
  visibility: hidden;
}

.vjs-seeking .vjs-loading-spinner,
.vjs-waiting .vjs-loading-spinner {
  display: block;
  // add a delay before actual show the spinner
  animation: vjs-spinner-show 0s linear 0.3s forwards;
}

.vjs-loading-spinner:before,
.vjs-loading-spinner:after {
  content: "";
  position: absolute;
  margin: -0.6em;
  box-sizing: inherit;
  width: inherit;
  height: inherit;
  border-radius: inherit;
  // Keep 100% opacity so they don't show through each other
  opacity: 1;
  border: inherit;
  border-color: transparent;
  border-top-color: white;
}

// only animate when showing because it can be processor heavy
.vjs-seeking .vjs-loading-spinner:before,
.vjs-seeking .vjs-loading-spinner:after,
.vjs-waiting .vjs-loading-spinner:before,
.vjs-waiting .vjs-loading-spinner:after {
  animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite;
}

.vjs-seeking .vjs-loading-spinner:before,
.vjs-waiting .vjs-loading-spinner:before {
  border-top-color: rgb(255,255,255);
}

.vjs-seeking .vjs-loading-spinner:after,
.vjs-waiting .vjs-loading-spinner:after {
  border-top-color: rgb(255,255,255);
  animation-delay: 0.44s;
}

@keyframes vjs-spinner-show {
  to {
    visibility: visible;
  }
}

@keyframes vjs-spinner-spin {
  100% {
    transform: rotate(360deg);
  }
}

@keyframes vjs-spinner-fade {
  0% {
    border-top-color: $secondary-background-color;
  }
  20% {
    border-top-color: $secondary-background-color;
  }
  35% {
    border-top-color: white;
  }
  60% {
    border-top-color: $secondary-background-color;
  }
  100% {
    border-top-color: $secondary-background-color;
  }
}


<!DOCTYPE html>

        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title></title>

        <link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700' rel='stylesheet' type='text/css'>

        <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/atelier-forest.light.min.css">
        <link rel="stylesheet" type="text/css" href="css/api-docs.css">

    <link rel="canonical" href="https://docs.videojs.com/Texttrack.html">

This documentation is for an outdated version of Video.js. See documentation for the current release.


var remarkrc = {
  settings: {
    bullet: '*',
    fence: '`',
    strong: '*',
    emphasis: '_',
    listItemIndent: 1,
    incrementListMarker: false
  },
  plugins: {
    'toc': {
      tight: true
    },
  }
};
var args = process.argv;

// only lint in non-output mode
if (args.indexOf('-o') === -1 && args.indexOf('--output') === -1) {
  remarkrc['validate-links'] = {};
  remarkrc.plugins.lint = {
    'blockquote-indentation': ['error', 2],
    'checkbox-character-style': ['warn'],
    'checkbox-content-indent': ['error'],
    'code-block-style': ['error', 'fenced'],
    'definition-case': ['off'],
    'definition-spacing': ['error'],
    'emphasis-marker': ['error', '_'],
    'fenced-code-flag': ['error'],
    'fenced-code-marker': ['error', '`'],
    'file-extension': ['error'],
    'final-definition': ['error'],
    'final-newline': ['off'],
    'first-heading-level': ['warn', 1],
    'hard-break-spaces': ['off'],
    'heading-increment': ['error'],
    'heading-style': ['error', 'atx'],
    'link-title-style': ['warn', '"'],
    'list-item-bullet-indent': ['error'],
    'list-item-content-indent': ['warn'],
    'list-item-indent': ['error', 'space'],
    'list-item-spacing': ['off'],
    'maximum-heading-length': ['off'],
    'maximum-line-length': ['off'],
    'no-auto-link-without-protocol': ['error'],
    'no-blockquote-without-caret': ['error'],
    'no-consecutive-blank-lines': ['error'],
    'no-duplicate-definitions': ['error'],
    'no-duplicate-headings-in-section': ['error'],
    'no-duplicate-headings': ['off'],
    'no-emphasis-as-heading': ['error'],
    'no-file-name-articles': ['off'],
    'no-file-name-consecutive-dashes': ['off'],
    'no-file-name-irregular-characters': ['warn', '\\.a-zA-Z0-9-_'],
    'no-file-name-mixed-case': ['error'],
    'no-file-name-outer-dashes': ['error'],
    'no-heading-content-indent': ['error'],
    'no-heading-indent': ['error'],
    'no-heading-punctuation': ['off'],
    'no-html': ['off'],
    'no-inline-padding': ['error'],
    'no-literal-urls': ['off'],
    'no-missing-blank-lines': ['off'],
    'no-multiple-toplevel-headings': ['error'],
    'no-reference-like-url': ['error'],
    'no-shell-dollars': ['error'],
    'no-shortcut-reference-iamge': ['off'],
    'no-shortcut-reference-link': ['off'],
    'no-table-indentation': ['error'],
    'no-tabs': ['error'],
    'no-undefined-references': ['error'],
    'no-unused-definitions': ['error'],
    'ordered-list-marker-style': ['error', '.'],
    'ordered-list-marker-value': ['error', 'one'],
    'rule-style': ['error', '***'],
    'strong-marker': ['error', '*'],
    'table-cell-padding': ['warn', 'padded'],
    'table-cell-alignment': ['warn'],
    'table-pipes': ['warn'],
    'unordered-list-marker-style': ['warn', '*']
  };
}

module.exports = remarkrc;


.DS_Store
build/files/*
build/temp/*
docs/api/*
docs/apistyles
dev.html
projects
.zenflow-log
test/*.map
.bunyipconfig.js
.s3config.json
.eslintcache

node_modules
npm-debug.log*

sandbox/*
!sandbox/*.example

*.swp
*.swo

*.orig

*results.xml
*.log

test/dist/*
.coveralls.yml
.sass-cache

dist/*
es5/*

.idea/

core.js
core.es.js

# Ignore Chinese clones for now.
lang/zh-Han*.json

# netlify deploy
deploy/


// Hide if an error occurs
.vjs-error .vjs-text-track-display {
display: none;
}

// Emulated tracks
.vjs-text-track-display {
  position: absolute;
  bottom: 3em;
  left: 0;
  right: 0;
  top: 0;
  pointer-events: none;
}

// Move captions down when controls aren't being shown
.video-js.vjs-controls-disabled .vjs-text-track-display,
.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display {
  bottom: 1em;
}

// Individual tracks
.video-js .vjs-text-track {
  font-size: 1.4em;
  text-align: center;
  margin-bottom: 0.1em;
}

.vjs-subtitles { color: #fff; } // Subtitles are white
.vjs-captions { color: #fc6; } // Captions are yellow
.vjs-tt-cue { display: block; }

// Native tracks
video::-webkit-media-text-track-display {
  @include transform(translateY(-3em));
}

// Move captions down when controls aren't being shown
.video-js.vjs-controls-disabled video::-webkit-media-text-track-display,
.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display {
  @include transform(translateY(-1.5em));
}


/* eslint-disable no-console */

const unified = require('unified');
const markdown = require('remark-parse');
const stringify = require('remark-stringify');
const fs = require('fs');

module.exports = function() {
  const processor = unified()
    .use(markdown, {commonmark: true})
    .use(stringify);

  const ast = processor.parse(fs.readFileSync('./CHANGELOG.md'));
  const changelog = [];

  changelog.push(processor.stringify(ast.children[0]));

  // start at 1 so we get the first anchor tag
  // and can break on the second
  for (let i = 1; i < ast.children.length; i++) {
    let item = processor.stringify(ast.children[i]);

    if (/^<a name="/.test(item)) {
      break;
    }

    if (/^###/.test(item)) {
      item = '\n' + item + '\n';
    }

    changelog.push(item);
  }

  return changelog.join('\n');
};