<commit_before>define(function (require) {

    // Brackets modules
    var _ = brackets.getModule("thirdparty/lodash"),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        FileUtils = brackets.getModule("file/FileUtils"),
        Mustache = brackets.getModule("thirdparty/mustache/mustache");

    // Local modules
    var moment = require("moment"),
        Strings = require("strings"),
        ErrorHandler = require("src/ErrorHandler"),
        Events = require("src/Events"),
        EventEmitter = require("src/EventEmitter"),
        Git = require("src/git/Git"),
        HistoryViewer = require("src/HistoryViewer"),
        Preferences = require("src/Preferences");

    // Templates
    var gitPanelHistoryTemplate = require("text!templates/git-panel-history.html"),
        gitPanelHistoryCommitsTemplate = require("text!templates/git-panel-history-commits.html");

    // Module variables
    var $gitPanel         = $(null),
        $tableContainer   = $(null),
        $historyList      = $(null),
        commitCache       = [],
        avatarType        = Preferences.get("avatarType"),
        lastDocumentSeen  = null;

    if (avatarType === "PICTURE" || avatarType === "IDENTICON") {
        var md5;
        require(["md5"], function (_md5) {
            md5 = _md5;
        });
    }

    // Implementation

    function initVariables() {
        $gitPanel = $("#git-panel");
        $tableContainer = $gitPanel.find(".table-container");
        attachHandlers();
    }

    function attachHandlers() {
        $tableContainer
            .off(".history")
            .on("scroll.history", function () {
                loadMoreHistory();
            })
            .on("click.history", ".history-commit", function () {
                var hash = $(this).attr("x-hash");
                var commit = _.find(commitCache, function (commit) { return commit.hash === hash; });
                HistoryViewer.show(commit, getCurrentDocument(), {
                    isInitial: $(this).attr("x-initial-commit") === "true"
                });
            });
    }

    var generateCssAvatar = _.memoize(function (author, email) {

        // Original source: http://indiegamr.com/generate-repeatable-random-numbers-in-js/
        var seededRandom = function (max, min, seed) {
            max = max || 1;
            min = min || 0;

            seed = (seed * 9301 + 49297) % 233280;
            var rnd = seed / 233280.0;

            return min + rnd * (max - min);
        };

        // Use `seededRandom()` to generate a pseudo-random number [0-16] to pick a color from the list
        var seedBase = parseInt(author.charCodeAt(3).toString(), email.length),
            seed = parseInt(email.charCodeAt(seedBase.toString().substring(1, 2)).toString(), 16),
            colors = [
                "#ffb13b", "#dd5f7a", "#8dd43a", "#2f7e2f", "#4141b9", "#3dafea", "#7e3e3e", "#f2f26b",
                "#864ba3", "#ac8aef", "#f2f2ce", "#379d9d", "#ff6750", "#8691a2", "#d2fd8d", "#88eadf"
            ],
            texts = [
                "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#333333",
                "#FEFEFE", "#FEFEFE", "#333333", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#333333", "#333333"
            ],
            picked = Math.floor(seededRandom(0, 16, seed));

        return "background-color: " + colors[picked] + "; color: " + texts[picked];

    }, function (author, email) {
        // calculate hash for memoize - both are strings so we don't need to convert
        return author + email;
    });

    var generateMd5 = _.memoize(function (string) {
        return md5(string);
    });

    // Render history list the first time
    function renderHistory(file) {
        // clear cache
        commitCache = [];

        return Git.getCurrentBranchName().then(function (branchName) {
            // Get the history commits of the current branch
            var p = file ? Git.getFileHistory(file.relative, branchName) : Git.getHistory(branchName);
            return p.then(function (commits) {

                // calculate some missing stuff like avatars
                commits = addAdditionalCommitInfo(commits);
                commitCache = commitCache.concat(commits);

                var templateData = {
                    commits: commits,
                    usePicture: avatarType === "PICTURE",
                    useIdenticon: avatarType === "IDENTICON",
                    useBwAvatar: avatarType === "AVATAR_BW",
                    useColoredAvatar: avatarType === "AVATAR_COLOR",
                    Strings: Strings
                };

                $tableContainer.append(Mustache.render(gitPanelHistoryTemplate, templateData, {
                    commits: gitPanelHistoryCommitsTemplate
                }));

                $historyList = $tableContainer.find("#git-history-list")
                    .data("file", file ? file.absolute : null)
                    .data("file-relative", file ? file.relative : null);
            });
        }).catch(function (err) {
            ErrorHandler.showError(err, "Failed to get history");
        });
    }

    // Load more rows in the history list on scroll
    function loadMoreHistory() {
        if ($historyList.is(":visible")) {
            if (($tableContainer.prop("scrollHeight") - $tableContainer.scrollTop()) === $tableContainer.height()) {
                if ($historyList.attr("x-finished") === "true") {
                    return;
                }
                return Git.getCurrentBranchName().then(function (branchName) {
                    var p,
                        file = $historyList.data("file-relative"),
                        skipCount = $tableContainer.find("tr.history-commit").length;
                    if (file) {
                        p = Git.getFileHistory(file, branchName, skipCount);
                    } else {
                        p = Git.getHistory(branchName, skipCount);
                    }
                    return p.then(function (commits) {
                        if (commits.length === 0) {
                            $historyList.attr("x-finished", "true");
                            // marks initial commit as first
                            $historyList
                                .find("tr.history-commit:last-child")
                                .attr("x-initial-commit", "true");
                            return;
                        }

                        commits = addAdditionalCommitInfo(commits);
                        commitCache = commitCache.concat(commits);

                        var templateData = {
                            commits: commits,
                            usePicture: avatarType === "PICTURE",
                            useIdenticon: avatarType === "IDENTICON",
                            useBwAvatar: avatarType === "AVATAR_BW",
                            useColoredAvatar: avatarType === "AVATAR_COLOR",
                            Strings: Strings
                        };
                        var commitsHtml = Mustache.render(gitPanelHistoryCommitsTemplate, templateData);
                        $historyList.children("tbody").append(commitsHtml);
                    })
                    .catch(function (err) {
                        ErrorHandler.showError(err, "Failed to load more history rows");
                    });
                })
                .catch(function (err) {
                    ErrorHandler.showError(err, "Failed to get current branch name");
                });
            }
        }
    }

    function addAdditionalCommitInfo(commits) {
        var mode        = Preferences.get("dateMode"),
            format      = Strings.DATE_FORMAT,
            // now         = moment(),
            // yesterday   = moment().subtract("d", 1).startOf("d"),
            ownFormat   = Preferences.get("dateFormat") || Strings.DATE_FORMAT;

        if (mode === 2 && format.indexOf(" ")) {
            // only date part
            format = format.substring(0, format.indexOf(" "));
        }

        _.forEach(commits, function (commit) {

            // Get color for AVATAR_BW and AVATAR_COLOR
            if (avatarType === "AVATAR_COLOR" || avatarType === "AVATAR_BW") {
                commit.cssAvatar = generateCssAvatar(commit.author, commit.email);
                commit.avatarLetter = commit.author.substring(0, 1);
            }
            if (avatarType === "PICTURE" || avatarType === "IDENTICON") {
                commit.emailHash = generateMd5(commit.email);
            }

            // FUTURE: convert date modes to sensible constant strings
            if (mode === 4) {
                // mode 4: Original Git date
                commit.date = {
                    shown: commit.date
                };
                return;
            }

            var date = moment(commit.date);
            commit.date = {
                title: ""
            };
            switch (mode) {
                // mode 0 (default): formatted with Strings.DATE_FORMAT
                default:
                case 0:
                    commit.date.shown = date.format(format);
                    break;
                // mode 1: always relative
                case 1:
                    commit.date.shown = date.fromNow();
                    commit.date.title = date.format(format);
                    break;
                // mode 2: intelligent relative/formatted
                case 2:
                    var relative = date.fromNow(),
                        formatted = date.format(format);
                    commit.date.shown = relative + " (" + formatted + ")";
                    commit.date.title = date.format(Strings.DATE_FORMAT);
                    /*
                    if (date.diff(yesterday) > 0) {
                        commit.date.shown = moment.duration(Math.max(date.diff(now), -24 * 60 * 60 * 1000), "ms").humanize(true);
                        commit.date.title = date.format(format);
                    } else {
                        commit.date.shown = date.format(format);
                    }
                    */
                    break;
                // mode 3: formatted with own format (as pref)
                case 3:
                    commit.date.shown = date.format(ownFormat);
                    commit.date.title = date.format(format);
                    break;
                /* mode 4 (Original Git date) is handled above */
            }
            commit.hasTag = (commit.tags) ? true : false;
        });

        return commits;
    }

    function getCurrentDocument() {
        if (HistoryViewer.isVisible()) {
            return lastDocumentSeen;
        }
        var doc = DocumentManager.getCurrentDocument();
        if (doc) {
            lastDocumentSeen = doc;
        }
        return doc || lastDocumentSeen;
    }

    function handleFileChange() {
        var currentDocument = getCurrentDocument();

        if ($historyList.is(":visible") && $historyList.data("file")) {
            handleToggleHistory("FILE", currentDocument);
        }
        $gitPanel.find(".git-file-history").prop("disabled", !currentDocument);
    }

    // Show or hide the history list on click of .history button
    // newHistoryMode can be "FILE" or "GLOBAL"
    function handleToggleHistory(newHistoryMode, newDocument) {
        // this is here to check that $historyList is still attached to the DOM
        $historyList = $tableContainer.find("#git-history-list");

        var historyEnabled = $historyList.is(":visible"),
            currentFile = $historyList.data("file") || null,
            currentHistoryMode = historyEnabled ? (currentFile ? "FILE" : "GLOBAL") : "DISABLED",
            doc = newDocument ? newDocument : getCurrentDocument(),
            file;

        if (currentHistoryMode !== newHistoryMode) {
            // we are switching the modes so enable
            historyEnabled = true;
        } else if (!newDocument) {
            // we are not changing the mode and we are not switching to a new document
            historyEnabled = !historyEnabled;
        }

        if (historyEnabled && newHistoryMode === "FILE") {
            if (doc) {
                file = {};
                file.absolute = doc.file.fullPath;
                file.relative = FileUtils.getRelativeFilename(Preferences.get("currentGitRoot"), file.absolute);
            } else {
                // we want a file history but no file was found
                historyEnabled = false;
            }
        }

        // Render #git-history-list if is not already generated or if the viewed file for file history has changed
        var isEmpty = $historyList.find("tr").length === 0,
            fileChanged = currentFile !== (file ? file.absolute : null);
        if (historyEnabled && (isEmpty || fileChanged)) {
            if ($historyList.length > 0) {
                $historyList.remove();
            }
            var $spinner = $("

").appendTo($gitPanel);
            renderHistory(file).finally(function () {
                $spinner.remove();
            });
        }

        // disable commit button when viewing history
        // refresh status when history is closed and commit button will correct its disabled state if required
        if (historyEnabled) {
            $gitPanel.find(".git-commit, .check-all").prop("disabled", true);
        } else {
            Git.status();
        }

        // Toggle visibility of .git-edited-list and #git-history-list
        $tableContainer.find(".git-edited-list").toggle(!historyEnabled);
        $historyList.toggle(historyEnabled);

        if (!historyEnabled) { HistoryViewer.hide(); }

        // Toggle history button
        var globalButtonActive  = historyEnabled && newHistoryMode === "GLOBAL",
            fileButtonActive    = historyEnabled && newHistoryMode === "FILE";
        $gitPanel.find(".git-history-toggle").toggleClass("active", globalButtonActive)
            .attr("title", globalButtonActive ? Strings.TOOLTIP_HIDE_HISTORY : Strings.TOOLTIP_SHOW_HISTORY);
        $gitPanel.find(".git-file-history").toggleClass("active", fileButtonActive)
            .attr("title", fileButtonActive ? Strings.TOOLTIP_HIDE_FILE_HISTORY : Strings.TOOLTIP_SHOW_FILE_HISTORY);
    }

    // Event listeners
    EventEmitter.on(Events.GIT_ENABLED, function () {
        initVariables();
    });
    EventEmitter.on(Events.GIT_DISABLED, function () {
        lastDocumentSeen = null;
        $historyList.remove();
        $historyList = $();
    });
    EventEmitter.on(Events.HISTORY_SHOW, function (mode) {
        handleToggleHistory(mode === "FILE" ? "FILE" : "GLOBAL");
    });
    EventEmitter.on(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, function () {
        handleFileChange();
    });

});
<commit_msg>Mark initial commit to fix error showing commit details<commit_after>define(function (require) {

    // Brackets modules
    var _ = brackets.getModule("thirdparty/lodash"),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        FileUtils = brackets.getModule("file/FileUtils"),
        Mustache = brackets.getModule("thirdparty/mustache/mustache");

    // Local modules
    var moment = require("moment"),
        Strings = require("strings"),
        ErrorHandler = require("src/ErrorHandler"),
        Events = require("src/Events"),
        EventEmitter = require("src/EventEmitter"),
        Git = require("src/git/Git"),
        HistoryViewer = require("src/HistoryViewer"),
        Preferences = require("src/Preferences");

    // Templates
    var gitPanelHistoryTemplate = require("text!templates/git-panel-history.html"),
        gitPanelHistoryCommitsTemplate = require("text!templates/git-panel-history-commits.html");

    // Module variables
    var $gitPanel         = $(null),
        $tableContainer   = $(null),
        $historyList      = $(null),
        commitCache       = [],
        avatarType        = Preferences.get("avatarType"),
        lastDocumentSeen  = null;

    if (avatarType === "PICTURE" || avatarType === "IDENTICON") {
        var md5;
        require(["md5"], function (_md5) {
            md5 = _md5;
        });
    }

    // Implementation

    function initVariables() {
        $gitPanel = $("#git-panel");
        $tableContainer = $gitPanel.find(".table-container");
        attachHandlers();
    }

    function attachHandlers() {
        $tableContainer
            .off(".history")
            .on("scroll.history", function () {
                loadMoreHistory();
            })
            .on("click.history", ".history-commit", function () {
                var hash = $(this).attr("x-hash");
                var commit = _.find(commitCache, function (commit) { return commit.hash === hash; });
                HistoryViewer.show(commit, getCurrentDocument(), {
                    isInitial: $(this).attr("x-initial-commit") === "true"
                });
            });
    }

    var generateCssAvatar = _.memoize(function (author, email) {

        // Original source: http://indiegamr.com/generate-repeatable-random-numbers-in-js/
        var seededRandom = function (max, min, seed) {
            max = max || 1;
            min = min || 0;

            seed = (seed * 9301 + 49297) % 233280;
            var rnd = seed / 233280.0;

            return min + rnd * (max - min);
        };

        // Use `seededRandom()` to generate a pseudo-random number [0-16] to pick a color from the list
        var seedBase = parseInt(author.charCodeAt(3).toString(), email.length),
            seed = parseInt(email.charCodeAt(seedBase.toString().substring(1, 2)).toString(), 16),
            colors = [
                "#ffb13b", "#dd5f7a", "#8dd43a", "#2f7e2f", "#4141b9", "#3dafea", "#7e3e3e", "#f2f26b",
                "#864ba3", "#ac8aef", "#f2f2ce", "#379d9d", "#ff6750", "#8691a2", "#d2fd8d", "#88eadf"
            ],
            texts = [
                "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#333333",
                "#FEFEFE", "#FEFEFE", "#333333", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#333333", "#333333"
            ],
            picked = Math.floor(seededRandom(0, 16, seed));

        return "background-color: " + colors[picked] + "; color: " + texts[picked];

    }, function (author, email) {
        // calculate hash for memoize - both are strings so we don't need to convert
        return author + email;
    });

    var generateMd5 = _.memoize(function (string) {
        return md5(string);
    });

    // Render history list the first time
    function renderHistory(file) {
        // clear cache
        commitCache = [];

        return Git.getCurrentBranchName().then(function (branchName) {
            // Get the history commits of the current branch
            var p = file ? Git.getFileHistory(file.relative, branchName) : Git.getHistory(branchName);
            return p.then(function (commits) {

                // calculate some missing stuff like avatars
                commits = addAdditionalCommitInfo(commits);
                commitCache = commitCache.concat(commits);

                var templateData = {
                    commits: commits,
                    usePicture: avatarType === "PICTURE",
                    useIdenticon: avatarType === "IDENTICON",
                    useBwAvatar: avatarType === "AVATAR_BW",
                    useColoredAvatar: avatarType === "AVATAR_COLOR",
                    Strings: Strings
                };

                $tableContainer.append(Mustache.render(gitPanelHistoryTemplate, templateData, {
                    commits: gitPanelHistoryCommitsTemplate
                }));

                $historyList = $tableContainer.find("#git-history-list")
                    .data("file", file ? file.absolute : null)
                    .data("file-relative", file ? file.relative : null);

                $historyList
                    .find("tr.history-commit:last-child")
                    .attr("x-initial-commit", "true");
            });
        }).catch(function (err) {
            ErrorHandler.showError(err, "Failed to get history");
        });
    }

    // Load more rows in the history list on scroll
    function loadMoreHistory() {
        if ($historyList.is(":visible")) {
            if (($tableContainer.prop("scrollHeight") - $tableContainer.scrollTop()) === $tableContainer.height()) {
                if ($historyList.attr("x-finished") === "true") {
                    return;
                }
                return Git.getCurrentBranchName().then(function (branchName) {
                    var p,
                        file = $historyList.data("file-relative"),
                        skipCount = $tableContainer.find("tr.history-commit").length;
                    if (file) {
                        p = Git.getFileHistory(file, branchName, skipCount);
                    } else {
                        p = Git.getHistory(branchName, skipCount);
                    }
                    return p.then(function (commits) {
                        if (commits.length === 0) {
                            $historyList.attr("x-finished", "true");
                            // marks initial commit as first
                            $historyList
                                .find("tr.history-commit:last-child")
                                .attr("x-initial-commit", "true");
                            return;
                        }

                        commits = addAdditionalCommitInfo(commits);
                        commitCache = commitCache.concat(commits);

                        var templateData = {
                            commits: commits,
                            usePicture: avatarType === "PICTURE",
                            useIdenticon: avatarType === "IDENTICON",
                            useBwAvatar: avatarType === "AVATAR_BW",
                            useColoredAvatar: avatarType === "AVATAR_COLOR",
                            Strings: Strings
                        };
                        var commitsHtml = Mustache.render(gitPanelHistoryCommitsTemplate, templateData);
                        $historyList.children("tbody").append(commitsHtml);
                    })
                    .catch(function (err) {
                        ErrorHandler.showError(err, "Failed to load more history rows");
                    });
                })
                .catch(function (err) {
                    ErrorHandler.showError(err, "Failed to get current branch name");
                });
            }
        }
    }

    function addAdditionalCommitInfo(commits) {
        var mode        = Preferences.get("dateMode"),
            format      = Strings.DATE_FORMAT,
            // now         = moment(),
            // yesterday   = moment().subtract("d", 1).startOf("d"),
            ownFormat   = Preferences.get("dateFormat") || Strings.DATE_FORMAT;

        if (mode === 2 && format.indexOf(" ")) {
            // only date part
            format = format.substring(0, format.indexOf(" "));
        }

        _.forEach(commits, function (commit) {

            // Get color for AVATAR_BW and AVATAR_COLOR
            if (avatarType === "AVATAR_COLOR" || avatarType === "AVATAR_BW") {
                commit.cssAvatar = generateCssAvatar(commit.author, commit.email);
                commit.avatarLetter = commit.author.substring(0, 1);
            }
            if (avatarType === "PICTURE" || avatarType === "IDENTICON") {
                commit.emailHash = generateMd5(commit.email);
            }

            // FUTURE: convert date modes to sensible constant strings
            if (mode === 4) {
                // mode 4: Original Git date
                commit.date = {
                    shown: commit.date
                };
                return;
            }

            var date = moment(commit.date);
            commit.date = {
                title: ""
            };
            switch (mode) {
                // mode 0 (default): formatted with Strings.DATE_FORMAT
                default:
                case 0:
                    commit.date.shown = date.format(format);
                    break;
                // mode 1: always relative
                case 1:
                    commit.date.shown = date.fromNow();
                    commit.date.title = date.format(format);
                    break;
                // mode 2: intelligent relative/formatted
                case 2:
                    var relative = date.fromNow(),
                        formatted = date.format(format);
                    commit.date.shown = relative + " (" + formatted + ")";
                    commit.date.title = date.format(Strings.DATE_FORMAT);
                    /*
                    if (date.diff(yesterday) > 0) {
                        commit.date.shown = moment.duration(Math.max(date.diff(now), -24 * 60 * 60 * 1000), "ms").humanize(true);
                        commit.date.title = date.format(format);
                    } else {
                        commit.date.shown = date.format(format);
                    }
                    */
                    break;
                // mode 3: formatted with own format (as pref)
                case 3:
                    commit.date.shown = date.format(ownFormat);
                    commit.date.title = date.format(format);
                    break;
                /* mode 4 (Original Git date) is handled above */
            }
            commit.hasTag = (commit.tags) ? true : false;
        });

        return commits;
    }

    function getCurrentDocument() {
        if (HistoryViewer.isVisible()) {
            return lastDocumentSeen;
        }
        var doc = DocumentManager.getCurrentDocument();
        if (doc) {
            lastDocumentSeen = doc;
        }
        return doc || lastDocumentSeen;
    }

    function handleFileChange() {
        var currentDocument = getCurrentDocument();

        if ($historyList.is(":visible") && $historyList.data("file")) {
            handleToggleHistory("FILE", currentDocument);
        }
        $gitPanel.find(".git-file-history").prop("disabled", !currentDocument);
    }

    // Show or hide the history list on click of .history button
    // newHistoryMode can be "FILE" or "GLOBAL"
    function handleToggleHistory(newHistoryMode, newDocument) {
        // this is here to check that $historyList is still attached to the DOM
        $historyList = $tableContainer.find("#git-history-list");

        var historyEnabled = $historyList.is(":visible"),
            currentFile = $historyList.data("file") || null,
            currentHistoryMode = historyEnabled ? (currentFile ? "FILE" : "GLOBAL") : "DISABLED",
            doc = newDocument ? newDocument : getCurrentDocument(),
            file;

        if (currentHistoryMode !== newHistoryMode) {
            // we are switching the modes so enable
            historyEnabled = true;
        } else if (!newDocument) {
            // we are not changing the mode and we are not switching to a new document
            historyEnabled = !historyEnabled;
        }

        if (historyEnabled && newHistoryMode === "FILE") {
            if (doc) {
                file = {};
                file.absolute = doc.file.fullPath;
                file.relative = FileUtils.getRelativeFilename(Preferences.get("currentGitRoot"), file.absolute);
            } else {
                // we want a file history but no file was found
                historyEnabled = false;
            }
        }

        // Render #git-history-list if is not already generated or if the viewed file for file history has changed
        var isEmpty = $historyList.find("tr").length === 0,
            fileChanged = currentFile !== (file ? file.absolute : null);
        if (historyEnabled && (isEmpty || fileChanged)) {
            if ($historyList.length > 0) {
                $historyList.remove();
            }
            var $spinner = $("

").appendTo($gitPanel);
            renderHistory(file).finally(function () {
                $spinner.remove();
            });
        }

        // disable commit button when viewing history
        // refresh status when history is closed and commit button will correct its disabled state if required
        if (historyEnabled) {
            $gitPanel.find(".git-commit, .check-all").prop("disabled", true);
        } else {
            Git.status();
        }

        // Toggle visibility of .git-edited-list and #git-history-list
        $tableContainer.find(".git-edited-list").toggle(!historyEnabled);
        $historyList.toggle(historyEnabled);

        if (!historyEnabled) { HistoryViewer.hide(); }

        // Toggle history button
        var globalButtonActive  = historyEnabled && newHistoryMode === "GLOBAL",
            fileButtonActive    = historyEnabled && newHistoryMode === "FILE";
        $gitPanel.find(".git-history-toggle").toggleClass("active", globalButtonActive)
            .attr("title", globalButtonActive ? Strings.TOOLTIP_HIDE_HISTORY : Strings.TOOLTIP_SHOW_HISTORY);
        $gitPanel.find(".git-file-history").toggleClass("active", fileButtonActive)
            .attr("title", fileButtonActive ? Strings.TOOLTIP_HIDE_FILE_HISTORY : Strings.TOOLTIP_SHOW_FILE_HISTORY);
    }

    // Event listeners
    EventEmitter.on(Events.GIT_ENABLED, function () {
        initVariables();
    });
    EventEmitter.on(Events.GIT_DISABLED, function () {
        lastDocumentSeen = null;
        $historyList.remove();
        $historyList = $();
    });
    EventEmitter.on(Events.HISTORY_SHOW, function (mode) {
        handleToggleHistory(mode === "FILE" ? "FILE" : "GLOBAL");
    });
    EventEmitter.on(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, function () {
        handleFileChange();
    });

});
<|endoftext|>


<commit_before>( function( root, factory ) {
    "use strict";
    // Set up appropriately for the environment.
    // Start with AMD.
    if ( typeof define === 'function' && define.amd ) {
        define(
            [
                'underscore',
                'backbone'
            ],
            function( _, Backbone ) {
                root.ExoSuit = root.ExoSuit || {};
                root.ExoSuit.Mixins = root.ExoSuit.Mixins || {};
                return ( root.ExoSuit.Mixins.CappedSubsetMixin = factory( root, {}, _, Backbone ) );
            }
        );
    // Next for Node.js or CommonJS.
    } else if ( typeof exports !== 'undefined' ) {
        var _ = require( 'underscore' );
        var Backbone = require( 'backbone' );
        module.exports = factory( root, exports, _, Backbone );
    // Finally, as a browser global.
    } else {
        root.ExoSuit = root.ExoSuit || {};
        root.ExoSuit.Mixins = root.ExoSuit.Mixins || {};
        root.ExoSuit.Mixins.CappedSubsetMixin = factory( root, {}, root._, root.Backbone );
    }
}( this, function( root, CappedSubsetMixin, _, Backbone ) {
    "use strict";
    CappedSubsetMixin = function() {
        var orderMethods = [ 'first', 'last' ];

        // Internal functions
        function isInt( number ) {
            if ( typeof number !== 'number' || parseInt( number, 10 ) !== number ) {
                return false;
            }
            return true;
        }

        function contains( value, array ) {
            var doesContain = false;

            for ( var i = 0, len = array.length; i < len; i++ ) {
                if ( !doesContain && value === array[ i ] ) {
                    doesContain = true;
                }
            }

            return doesContain;
        }

        // Cache current mixin methods to use later
        var oldInitialize = this.initialize;

        // Set collection capped limit
        this.limit = isInt( this.limit ) ? this.limit : 100;
        this.order = contains( this.order, orderMethods ) ? this.order : 'last';

        this.initialize = function( models, options ) {
            options = options || {};

            if ( options.parent ) {
                this.parent = options.parent;
            }

            if ( !this.parent ) {
                throw new Error( 'Parent collection is required to use subset collection' );
            }

            this.model = this.parent.model;

            if ( options.limit && options.limit !== this.limit ) {
                this.resize( options.limit, { silent: true } );
            }

            if ( options.order && options.order !== this.order ) {
                this.reorder( options.order, { silent: true } );
            }

            this.listenTo( this.parent, {
                add: this.__add,
                remove: this.__remove,
                reset: this.__reset,
                sort: this.__reset
            } );

            this.__reset();

            if ( oldInitialize ) {
                oldInitialize.apply( this, arguments );
            }
        };

        this._getParentModels = function() {
            if ( !this.parent ) {
                return;
            }

            var limit = this.parent.length < this.limit ? this.parent.length : this.limit;

            return this.parent[ this.order ]( limit );
        };

        this.__add = function( model, collection, options ) {
            options = options || {};

            var index;

            if ( !this.comparator && typeof options.at !== 'undefined' ) {
                index = options.at;
            }

            if ( this.parent.length < this.limit ) {
                this.add( model );
            } else if ( this.order.toLowerCase() === 'last' && this.parent.indexOf( model ) >= ( this.parent.length - this.limit ) ) {
                this.remove( this.parent.at( this.parent.length - this.limit - 1 ) );
                this.add( model, { at : index ? index - this.limit : undefined } );
            } else if ( this.order.toLowerCase() === 'first' && this.parent.indexOf( model ) < this.limit ) {
                this.remove( this.parent.at( this.limit ) );
                this.add( model, { at : index } );
            }
        };

        this.__remove = function( model, collection, options ) {
            options = options || {};

            var parentModels;

            if ( this.contains( model ) ) {
                this.remove( model );
                if ( this.parent.length > this.length ) {
                    parentModels = this._getParentModels();
                    if ( this.order.toLowerCase() === 'last' ) {
                        if ( !this.comparator && typeof options.at === undefined ) {
                            options.at = 0 ;
                        }
                        this.add( parentModels[ 0 ], options );
                    } else {
                        this.add( parentModels[ this.limit - 1 ], options );
                    }
                }
            }
        };

        this.__reset = function() {
            this.reset( this._getParentModels() );
        };

        this.resize = function( size, options ) {
            // Make sure passed in size is a number
            if ( !isInt( size ) ) {
                throw new Error( 'Size must be a number' );
            }

            options = options || {};

            var models,
                modelsToProcess = [],
                newCids,
                oldLimit = this.limit;

            this.limit = size;
            models = this._getParentModels();
            newCids = _.pluck( models, 'cid' );

            if ( size > oldLimit ) {
                if ( this.order === 'last' ) {
                    modelsToProcess = models.slice( 0, this.limit - this.length );
                    if ( !this.comparator && typeof options.at === "undefined" ) {
                        options.at = 0 ;
                    }
                } else {
                    modelsToProcess = models.slice( this.length, this.limit );
                }
                this.add( modelsToProcess, options );
            } else if ( size < oldLimit ) {
                modelsToProcess = this.filter( function( model ) {
                    return !_.contains( newCids, model.cid );
                }, this );
                this.remove( modelsToProcess );
            }

            if ( !options.silent ) {
                this.trigger( 'resize', size, this, options );
            }
        };

        this.reorder = function( options ) {
            options = options || {};

            this.order = this.order === 'first' ? 'last' : 'first';

            this.reset( this._getParentModels() );

            if ( !options.silent ) {
                this.trigger( 'reorder', this.order, this, options );
            }
        };
    };

    return CappedSubsetMixin;
} ));
<commit_msg>Pass options argument through<commit_after>( function( root, factory ) {
    "use strict";
    // Set up appropriately for the environment.
    // Start with AMD.
    if ( typeof define === 'function' && define.amd ) {
        define(
            [
                'underscore',
                'backbone'
            ],
            function( _, Backbone ) {
                root.ExoSuit = root.ExoSuit || {};
                root.ExoSuit.Mixins = root.ExoSuit.Mixins || {};
                return ( root.ExoSuit.Mixins.CappedSubsetMixin = factory( root, {}, _, Backbone ) );
            }
        );
    // Next for Node.js or CommonJS.
    } else if ( typeof exports !== 'undefined' ) {
        var _ = require( 'underscore' );
        var Backbone = require( 'backbone' );
        module.exports = factory( root, exports, _, Backbone );
    // Finally, as a browser global.
    } else {
        root.ExoSuit = root.ExoSuit || {};
        root.ExoSuit.Mixins = root.ExoSuit.Mixins || {};
        root.ExoSuit.Mixins.CappedSubsetMixin = factory( root, {}, root._, root.Backbone );
    }
}( this, function( root, CappedSubsetMixin, _, Backbone ) {
    "use strict";
    CappedSubsetMixin = function() {
        var orderMethods = [ 'first', 'last' ];

        // Internal functions
        function isInt( number ) {
            if ( typeof number !== 'number' || parseInt( number, 10 ) !== number ) {
                return false;
            }
            return true;
        }

        function contains( value, array ) {
            var doesContain = false;

            for ( var i = 0, len = array.length; i < len; i++ ) {
                if ( !doesContain && value === array[ i ] ) {
                    doesContain = true;
                }
            }

            return doesContain;
        }

        // Cache current mixin methods to use later
        var oldInitialize = this.initialize;

        // Set collection capped limit
        this.limit = isInt( this.limit ) ? this.limit : 100;
        this.order = contains( this.order, orderMethods ) ? this.order : 'last';

        this.initialize = function( models, options ) {
            options = options || {};

            if ( options.parent ) {
                this.parent = options.parent;
            }

            if ( !this.parent ) {
                throw new Error( 'Parent collection is required to use subset collection' );
            }

            this.model = this.parent.model;

            if ( options.limit && options.limit !== this.limit ) {
                this.resize( options.limit, { silent: true } );
            }

            if ( options.order && options.order !== this.order ) {
                this.reorder( options.order, { silent: true } );
            }

            this.listenTo( this.parent, {
                add: this.__add,
                remove: this.__remove,
                reset: this.__reset,
                sort: this.__reset
            } );

            this.__reset();

            if ( oldInitialize ) {
                oldInitialize.apply( this, arguments );
            }
        };

        this._getParentModels = function() {
            if ( !this.parent ) {
                return;
            }

            var limit = this.parent.length < this.limit ? this.parent.length : this.limit;

            return this.parent[ this.order ]( limit );
        };

        this.__add = function( model, collection, options ) {
            options = options || {};

            var index;

            if ( !this.comparator && typeof options.at !== 'undefined' ) {
                index = options.at;
            }

            if ( this.parent.length < this.limit ) {
                this.add( model, options );
            } else if ( this.order.toLowerCase() === 'last' && this.parent.indexOf( model ) >= ( this.parent.length - this.limit ) ) {
                this.remove( this.parent.at( this.parent.length - this.limit - 1 ), options );
                options.at = index ? index - this.limit : undefined;
                this.add( model, options );
            } else if ( this.order.toLowerCase() === 'first' && this.parent.indexOf( model ) < this.limit ) {
                this.remove( this.parent.at( this.limit ), options );
                this.add( model, options );
            }
        };

        this.__remove = function( model, collection, options ) {
            options = options || {};

            var parentModels;

            if ( this.contains( model ) ) {
                this.remove( model, options );
                if ( this.parent.length > this.length ) {
                    parentModels = this._getParentModels();
                    if ( this.order.toLowerCase() === 'last' ) {
                        if ( !this.comparator && typeof options.at === undefined ) {
                            options.at = 0 ;
                        }
                        this.add( parentModels[ 0 ], options );
                    } else {
                        this.add( parentModels[ this.limit - 1 ], options );
                    }
                }
            }
        };

        this.__reset = function( collection, options ) {
            this.reset( this._getParentModels(), options );
        };

        this.resize = function( size, options ) {
            // Make sure passed in size is a number
            if ( !isInt( size ) ) {
                throw new Error( 'Size must be a number' );
            }

            options = options || {};

            var models,
                modelsToProcess = [],
                newCids,
                oldLimit = this.limit;

            this.limit = size;
            models = this._getParentModels();
            newCids = _.pluck( models, 'cid' );

            if ( size > oldLimit ) {
                if ( this.order === 'last' ) {
                    modelsToProcess = models.slice( 0, this.limit - this.length );
                    if ( !this.comparator && typeof options.at === "undefined" ) {
                        options.at = 0 ;
                    }
                } else {
                    modelsToProcess = models.slice( this.length, this.limit );
                }
                this.add( modelsToProcess, options );
            } else if ( size < oldLimit ) {
                modelsToProcess = this.filter( function( model ) {
                    return !_.contains( newCids, model.cid );
                }, this );
                this.remove( modelsToProcess );
            }

            if ( !options.silent ) {
                this.trigger( 'resize', size, this, options );
            }
        };

        this.reorder = function( options ) {
            options = options || {};

            this.order = this.order === 'first' ? 'last' : 'first';

            this.reset( this._getParentModels() );

            if ( !options.silent ) {
                this.trigger( 'reorder', this.order, this, options );
            }
        };
    };

    return CappedSubsetMixin;
} ));
<|endoftext|>


<commit_before>'use strict';

/**
 * Module dependencies.
 */
var should = require('should'),
    mongoose = require('mongoose'),
    Genre = mongoose.model('Genre');

/**
 * Unit tests
 */
describe('Genre Model', function() {

    describe('Saving', function() {
        it('saves new record');

        it('throws validation error when name is empty');

        it('throws validation error when name longer than 15 chars');

        it('throws validation error for duplicate genre name');
    });

});<commit_msg>Populated Genre tests with actual functions, rather than stubbed methods<commit_after>'use strict';

/**
 * Module dependencies.
 */
var should = require('should'),
    mongoose = require('mongoose'),
    Genre = mongoose.model('Genre');

/**
 * Unit tests
 */
describe('Genre Model', function() {
    describe('Saving', function() {
        it('saves new record', function(done) {
            var category = new Genre({
                name: 'Action-test',
                description: 'Movies that are categorized by the emphasis on dramatic action sequences'
            });

            category.save(function(err, saved) {
                should.not.exist(err);
                done();
            });
        });

        it('throws validation error when name is empty', function(done) {
            var category = new Genre({
                description: 'Movies that are categorized by the emphasis on dramatic action sequences'
            });

            category.save(function(err) {
                should.exist(err);
                err.errors.name.message.should.equal('name cannot be blank');
                done();
            });
        });

        it('throws validation error when name longer than 15 chars', function(done) {
            var category = new Genre({
                name: 'Super Awesome Exciting Action Fun Time'
            });

            category.save(function(err, saved) {
                should.exist(err);
                err.errors.name.message.should.equal('name must be 15 chars in length or less');
                done();
            });
        });

        it('throws validation error for duplicate category name', function(done) {
            var category = new Genre({
                name: 'Action-test'
            });

            category.save(function(err) {
                should.not.exist(err);

                var duplicate = new Genre({
                    name: 'Action-test'
                });

                duplicate.save(function(err) {
                    err.err.indexOf('$name').should.not.equal(-1);
                    err.err.indexOf('duplicate key error').should.not.equal(-1);
                    should.exist(err);
                    done();
                });
            });
        });
    });

    afterEach(function(done) {
        // this deletes ALL categories (but is run against a test database)
        Genre.remove().exec();
        done();
    });
});<|endoftext|>


<commit_before>import Popper from 'popper.js'
import { Vue } from '../vue'
import { NAME_DROPDOWN } from '../constants/components'
import {
  EVENT_NAME_CLICK,
  EVENT_NAME_HIDDEN,
  EVENT_NAME_HIDE,
  EVENT_NAME_SHOW,
  EVENT_NAME_SHOWN,
  EVENT_NAME_TOGGLE
} from '../constants/events'
import { CODE_DOWN, CODE_ENTER, CODE_ESC, CODE_SPACE, CODE_UP } from '../constants/key-codes'
import {
  PLACEMENT_TOP_START,
  PLACEMENT_TOP_END,
  PLACEMENT_BOTTOM_START,
  PLACEMENT_BOTTOM_END,
  PLACEMENT_RIGHT_START,
  PLACEMENT_LEFT_START
} from '../constants/popper'
import {
  PROP_TYPE_BOOLEAN,
  PROP_TYPE_NUMBER_STRING,
  PROP_TYPE_OBJECT,
  PROP_TYPE_STRING
} from '../constants/props'
import { HTMLElement } from '../constants/safe-types'
import { BvEvent } from '../utils/bv-event.class'
import { attemptFocus, closest, contains, isVisible, requestAF, selectAll } from '../utils/dom'
import { getRootEventName, stopEvent } from '../utils/events'
import { isNull } from '../utils/inspect'
import { mergeDeep, sortKeys } from '../utils/object'
import { makeProp, makePropsConfigurable } from '../utils/props'
import { warn } from '../utils/warn'
import { clickOutMixin } from './click-out'
import { focusInMixin } from './focus-in'
import { idMixin, props as idProps } from './id'
import { listenOnRootMixin } from './listen-on-root'

// --- Constants ---

const ROOT_EVENT_NAME_SHOWN = getRootEventName(NAME_DROPDOWN, EVENT_NAME_SHOWN)
const ROOT_EVENT_NAME_HIDDEN = getRootEventName(NAME_DROPDOWN, EVENT_NAME_HIDDEN)

// CSS selectors
const SELECTOR_FORM_CHILD = '.dropdown form'
const SELECTOR_ITEM = ['.dropdown-item', '.b-dropdown-form']
  .map(selector => `${selector}:not(.disabled):not([disabled])`)
  .join(', ')

// --- Helper methods ---

// Return an array of visible items
const filterVisibles = els => (els || []).filter(isVisible)

// --- Props ---

export const props = makePropsConfigurable(
  sortKeys({
    ...idProps,
    // String: `scrollParent`, `window` or `viewport`
    // HTMLElement: HTML Element reference
    boundary: makeProp([HTMLElement, PROP_TYPE_STRING], 'scrollParent'),
    disabled: makeProp(PROP_TYPE_BOOLEAN, false),
    // Place left if possible
    dropleft: makeProp(PROP_TYPE_BOOLEAN, false),
    // Place right if possible
    dropright: makeProp(PROP_TYPE_BOOLEAN, false),
    // Place on top if possible
    dropup: makeProp(PROP_TYPE_BOOLEAN, false),
    // Disable auto-flipping of menu from bottom <=> top
    noFlip: makeProp(PROP_TYPE_BOOLEAN, false),
    // Number of pixels or a CSS unit value to offset menu
    // (i.e. `1px`, `1rem`, etc.)
    offset: makeProp(PROP_TYPE_NUMBER_STRING, 0),
    popperOpts: makeProp(PROP_TYPE_OBJECT, {}),
    // Right align menu (default is left align)
    right: makeProp(PROP_TYPE_BOOLEAN, false)
  }),
  NAME_DROPDOWN
)

// --- Mixin ---

// @vue/component
export const dropdownMixin = Vue.extend({
  mixins: [idMixin, listenOnRootMixin, clickOutMixin, focusInMixin],
  provide() {
    return { bvDropdown: this }
  },
  inject: {
    bvNavbar: { default: null }
  },
  props,
  data() {
    return {
      visible: false,
      visibleChangePrevented: false
    }
  },
  computed: {
    inNavbar() {
      return !isNull(this.bvNavbar)
    },
    toggler() {
      const { toggle } = this.$refs
      return toggle ? toggle.$el || toggle : null
    },
    directionClass() {
      if (this.dropup) {
        return 'dropup'
      } else if (this.dropright) {
        return 'dropright'
      } else if (this.dropleft) {
        return 'dropleft'
      }
      return ''
    },
    boundaryClass() {
      // Position `static` is needed to allow menu to "breakout" of the `scrollParent`
      // boundaries when boundary is anything other than `scrollParent`
      // See: https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
      return this.boundary !== 'scrollParent' && !this.inNavbar ? 'position-static' : ''
    }
  },
  watch: {
    visible(newValue, oldValue) {
      if (this.visibleChangePrevented) {
        this.visibleChangePrevented = false
        return
      }

      if (newValue !== oldValue) {
        const eventName = newValue ? EVENT_NAME_SHOW : EVENT_NAME_HIDE
        const bvEvent = new BvEvent(eventName, {
          cancelable: true,
          vueTarget: this,
          target: this.$refs.menu,
          relatedTarget: null,
          componentId: this.safeId ? this.safeId() : this.id || null
        })
        this.emitEvent(bvEvent)
        if (bvEvent.defaultPrevented) {
          // Reset value and exit if canceled
          this.visibleChangePrevented = true
          this.visible = oldValue
          // Just in case a child element triggered `this.hide(true)`
          this.$off(EVENT_NAME_HIDDEN, this.focusToggler)
          return
        }
        if (newValue) {
          this.showMenu()
        } else {
          this.hideMenu()
        }
      }
    },
    disabled(newValue, oldValue) {
      if (newValue !== oldValue && newValue && this.visible) {
        // Hide dropdown if disabled changes to true
        this.visible = false
      }
    }
  },
  created() {
    // Create private non-reactive props
    this.$_popper = null
    this.$_hideTimeout = null
  },
  /* istanbul ignore next */
  deactivated() {
    // In case we are inside a `<keep-alive>`
    this.visible = false
    this.whileOpenListen(false)
    this.destroyPopper()
  },
  beforeDestroy() {
    this.visible = false
    this.whileOpenListen(false)
    this.destroyPopper()
    this.clearHideTimeout()
  },
  methods: {
    // Event emitter
    emitEvent(bvEvent) {
      const { type } = bvEvent
      this.emitOnRoot(getRootEventName(NAME_DROPDOWN, type), bvEvent)
      this.$emit(type, bvEvent)
    },
    showMenu() {
      if (this.disabled) {
        /* istanbul ignore next */
        return
      }

      // Only instantiate Popper.js when dropdown is not in `<b-navbar>`
      if (!this.inNavbar) {
        if (typeof Popper === 'undefined') {
          /* istanbul ignore next */
          warn('Popper.js not found. Falling back to CSS positioning', NAME_DROPDOWN)
        } else {
          // For dropup with alignment we use the parent element as popper container
          let el = (this.dropup && this.right) || this.split ? this.$el : this.$refs.toggle
          // Make sure we have a reference to an element, not a component!
          el = el.$el || el
          // Instantiate Popper.js
          this.createPopper(el)
        }
      }

      // Ensure other menus are closed
      this.emitOnRoot(ROOT_EVENT_NAME_SHOWN, this)

      // Enable listeners
      this.whileOpenListen(true)

      // Wrap in `$nextTick()` to ensure menu is fully rendered/shown
      this.$nextTick(() => {
        // Focus on the menu container on show
        this.focusMenu()
        // Emit the shown event
        this.$emit(EVENT_NAME_SHOWN)
      })
    },
    hideMenu() {
      this.whileOpenListen(false)
      this.emitOnRoot(ROOT_EVENT_NAME_HIDDEN, this)
      this.$emit(EVENT_NAME_HIDDEN)
      this.destroyPopper()
    },
    createPopper(element) {
      this.destroyPopper()
      this.$_popper = new Popper(element, this.$refs.menu, this.getPopperConfig())
    },
    // Ensure popper event listeners are removed cleanly
    destroyPopper() {
      this.$_popper && this.$_popper.destroy()
      this.$_popper = null
    },
    // Instructs popper to re-computes the dropdown position
    // useful if the content changes size
    updatePopper() {
      try {
        this.$_popper.scheduleUpdate()
      } catch {}
    },
    clearHideTimeout() {
      clearTimeout(this.$_hideTimeout)
      this.$_hideTimeout = null
    },
    getPopperConfig() {
      let placement = PLACEMENT_BOTTOM_START
      if (this.dropup) {
        placement = this.right ? PLACEMENT_TOP_END : PLACEMENT_TOP_START
      } else if (this.dropright) {
        placement = PLACEMENT_RIGHT_START
      } else if (this.dropleft) {
        placement = PLACEMENT_LEFT_START
      } else if (this.right) {
        placement = PLACEMENT_BOTTOM_END
      }
      const popperConfig = {
        placement,
        modifiers: {
          offset: { offset: this.offset || 0 },
          flip: { enabled: !this.noFlip }
        }
      }
      const boundariesElement = this.boundary
      if (boundariesElement) {
        popperConfig.modifiers.preventOverflow = { boundariesElement }
      }
      return mergeDeep(popperConfig, this.popperOpts || {})
    },
    // Turn listeners on/off while open
    whileOpenListen(isOpen) {
      // Hide the dropdown when clicked outside
      this.listenForClickOut = isOpen
      // Hide the dropdown when it loses focus
      this.listenForFocusIn = isOpen
      // Hide the dropdown when another dropdown is opened
      const method = isOpen ? '$on' : '$off'
      this.$root[method](ROOT_EVENT_NAME_SHOWN, this.rootCloseListener)
    },
    rootCloseListener(vm) {
      if (vm !== this) {
        this.visible = false
      }
    },
    // Public method to show dropdown
    show() {
      if (this.disabled) {
        return
      }
      // Wrap in a `requestAF()` to allow any previous
      // click handling to occur first
      requestAF(() => {
        this.visible = true
      })
    },
    // Public method to hide dropdown
    hide(refocus = false) {
      /* istanbul ignore next */
      if (this.disabled) {
        return
      }
      this.visible = false
      if (refocus) {
        // Child element is closing the dropdown on click
        this.$once(EVENT_NAME_HIDDEN, this.focusToggler)
      }
    },
    // Called only by a button that toggles the menu
    toggle(event) {
      event = event || {}
      // Early exit when not a click event or ENTER, SPACE or DOWN were pressed
      const { type, keyCode } = event
      if (
        type !== 'click' &&
        !(type === 'keydown' && [CODE_ENTER, CODE_SPACE, CODE_DOWN].indexOf(keyCode) !== -1)
      ) {
        /* istanbul ignore next */
        return
      }
      /* istanbul ignore next */
      if (this.disabled) {
        this.visible = false
        return
      }
      this.$emit(EVENT_NAME_TOGGLE, event)
      stopEvent(event)
      // Toggle visibility
      if (this.visible) {
        this.hide(true)
      } else {
        this.show()
      }
    },
    // Mousedown handler for the toggle
    /* istanbul ignore next */
    onMousedown(event) {
      // We prevent the 'mousedown' event for the toggle to stop the
      // 'focusin' event from being fired
      // The event would otherwise be picked up by the global 'focusin'
      // listener and there is no cross-browser solution to detect it
      // relates to the toggle click
      // The 'click' event will still be fired and we handle closing
      // other dropdowns there too
      // See https://github.com/bootstrap-vue/bootstrap-vue/issues/4328
      stopEvent(event, { propagation: false })
    },
    // Called from dropdown menu context
    onKeydown(event) {
      const { keyCode } = event
      if (keyCode === CODE_ESC) {
        // Close on ESC
        this.onEsc(event)
      } else if (keyCode === CODE_DOWN) {
        // Down Arrow
        this.focusNext(event, false)
      } else if (keyCode === CODE_UP) {
        // Up Arrow
        this.focusNext(event, true)
      }
    },
    // If user presses ESC, close the menu
    onEsc(event) {
      if (this.visible) {
        this.visible = false
        stopEvent(event)
        // Return focus to original trigger button
        this.$once(EVENT_NAME_HIDDEN, this.focusToggler)
      }
    },
    // Called only in split button mode, for the split button
    onSplitClick(event) {
      /* istanbul ignore next */
      if (this.disabled) {
        this.visible = false
        return
      }
      this.$emit(EVENT_NAME_CLICK, event)
    },
    // Shared hide handler between click-out and focus-in events
    hideHandler(event) {
      const { target } = event
      if (this.visible && !contains(this.$refs.menu, target) && !contains(this.toggler, target)) {
        this.clearHideTimeout()
        this.$_hideTimeout = setTimeout(() => this.hide(), this.inNavbar ? 300 : 0)
      }
    },
    // Document click-out listener
    clickOutHandler(event) {
      this.hideHandler(event)
    },
    // Document focus-in listener
    focusInHandler(event) {
      this.hideHandler(event)
    },
    // Keyboard nav
    focusNext(event, up) {
      // Ignore key up/down on form elements
      const { target } = event
      if (!this.visible || (event && closest(SELECTOR_FORM_CHILD, target))) {
        /* istanbul ignore next: should never happen */
        return
      }
      stopEvent(event)
      this.$nextTick(() => {
        const items = this.getItems()
        if (items.length < 1) {
          /* istanbul ignore next: should never happen */
          return
        }
        let index = items.indexOf(target)
        if (up && index > 0) {
          index--
        } else if (!up && index < items.length - 1) {
          index++
        }
        if (index < 0) {
          /* istanbul ignore next: should never happen */
          index = 0
        }
        this.focusItem(index, items)
      })
    },
    focusItem(index, items) {
      const el = items.find((el, i) => i === index)
      attemptFocus(el)
    },
    getItems() {
      // Get all items
      return filterVisibles(selectAll(SELECTOR_ITEM, this.$refs.menu))
    },
    focusMenu() {
      attemptFocus(this.$refs.menu)
    },
    focusToggler() {
      this.$nextTick(() => {
        attemptFocus(this.toggler)
      })
    }
  }
})
<commit_msg>fix(b-dropdown): decrease delay when hiding inside a navbar on no-touch devices (closes #6306) (#6367)<commit_after>import Popper from 'popper.js'
import { Vue } from '../vue'
import { NAME_DROPDOWN } from '../constants/components'
import { HAS_TOUCH_SUPPORT } from '../constants/env'
import {
  EVENT_NAME_CLICK,
  EVENT_NAME_HIDDEN,
  EVENT_NAME_HIDE,
  EVENT_NAME_SHOW,
  EVENT_NAME_SHOWN,
  EVENT_NAME_TOGGLE
} from '../constants/events'
import { CODE_DOWN, CODE_ENTER, CODE_ESC, CODE_SPACE, CODE_UP } from '../constants/key-codes'
import {
  PLACEMENT_TOP_START,
  PLACEMENT_TOP_END,
  PLACEMENT_BOTTOM_START,
  PLACEMENT_BOTTOM_END,
  PLACEMENT_RIGHT_START,
  PLACEMENT_LEFT_START
} from '../constants/popper'
import {
  PROP_TYPE_BOOLEAN,
  PROP_TYPE_NUMBER_STRING,
  PROP_TYPE_OBJECT,
  PROP_TYPE_STRING
} from '../constants/props'
import { HTMLElement } from '../constants/safe-types'
import { BvEvent } from '../utils/bv-event.class'
import { attemptFocus, closest, contains, isVisible, requestAF, selectAll } from '../utils/dom'
import { getRootEventName, stopEvent } from '../utils/events'
import { isNull } from '../utils/inspect'
import { mergeDeep, sortKeys } from '../utils/object'
import { makeProp, makePropsConfigurable } from '../utils/props'
import { warn } from '../utils/warn'
import { clickOutMixin } from './click-out'
import { focusInMixin } from './focus-in'
import { idMixin, props as idProps } from './id'
import { listenOnRootMixin } from './listen-on-root'

// --- Constants ---

const ROOT_EVENT_NAME_SHOWN = getRootEventName(NAME_DROPDOWN, EVENT_NAME_SHOWN)
const ROOT_EVENT_NAME_HIDDEN = getRootEventName(NAME_DROPDOWN, EVENT_NAME_HIDDEN)

// CSS selectors
const SELECTOR_FORM_CHILD = '.dropdown form'
const SELECTOR_ITEM = ['.dropdown-item', '.b-dropdown-form']
  .map(selector => `${selector}:not(.disabled):not([disabled])`)
  .join(', ')

// --- Helper methods ---

// Return an array of visible items
const filterVisibles = els => (els || []).filter(isVisible)

// --- Props ---

export const props = makePropsConfigurable(
  sortKeys({
    ...idProps,
    // String: `scrollParent`, `window` or `viewport`
    // HTMLElement: HTML Element reference
    boundary: makeProp([HTMLElement, PROP_TYPE_STRING], 'scrollParent'),
    disabled: makeProp(PROP_TYPE_BOOLEAN, false),
    // Place left if possible
    dropleft: makeProp(PROP_TYPE_BOOLEAN, false),
    // Place right if possible
    dropright: makeProp(PROP_TYPE_BOOLEAN, false),
    // Place on top if possible
    dropup: makeProp(PROP_TYPE_BOOLEAN, false),
    // Disable auto-flipping of menu from bottom <=> top
    noFlip: makeProp(PROP_TYPE_BOOLEAN, false),
    // Number of pixels or a CSS unit value to offset menu
    // (i.e. `1px`, `1rem`, etc.)
    offset: makeProp(PROP_TYPE_NUMBER_STRING, 0),
    popperOpts: makeProp(PROP_TYPE_OBJECT, {}),
    // Right align menu (default is left align)
    right: makeProp(PROP_TYPE_BOOLEAN, false)
  }),
  NAME_DROPDOWN
)

// --- Mixin ---

// @vue/component
export const dropdownMixin = Vue.extend({
  mixins: [idMixin, listenOnRootMixin, clickOutMixin, focusInMixin],
  provide() {
    return { bvDropdown: this }
  },
  inject: {
    bvNavbar: { default: null }
  },
  props,
  data() {
    return {
      visible: false,
      visibleChangePrevented: false
    }
  },
  computed: {
    inNavbar() {
      return !isNull(this.bvNavbar)
    },
    toggler() {
      const { toggle } = this.$refs
      return toggle ? toggle.$el || toggle : null
    },
    directionClass() {
      if (this.dropup) {
        return 'dropup'
      } else if (this.dropright) {
        return 'dropright'
      } else if (this.dropleft) {
        return 'dropleft'
      }
      return ''
    },
    boundaryClass() {
      // Position `static` is needed to allow menu to "breakout" of the `scrollParent`
      // boundaries when boundary is anything other than `scrollParent`
      // See: https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
      return this.boundary !== 'scrollParent' && !this.inNavbar ? 'position-static' : ''
    },
    hideDelay() {
      return this.inNavbar ? (HAS_TOUCH_SUPPORT ? 300 : 50) : 0
    }
  },
  watch: {
    visible(newValue, oldValue) {
      if (this.visibleChangePrevented) {
        this.visibleChangePrevented = false
        return
      }

      if (newValue !== oldValue) {
        const eventName = newValue ? EVENT_NAME_SHOW : EVENT_NAME_HIDE
        const bvEvent = new BvEvent(eventName, {
          cancelable: true,
          vueTarget: this,
          target: this.$refs.menu,
          relatedTarget: null,
          componentId: this.safeId ? this.safeId() : this.id || null
        })
        this.emitEvent(bvEvent)
        if (bvEvent.defaultPrevented) {
          // Reset value and exit if canceled
          this.visibleChangePrevented = true
          this.visible = oldValue
          // Just in case a child element triggered `this.hide(true)`
          this.$off(EVENT_NAME_HIDDEN, this.focusToggler)
          return
        }
        if (newValue) {
          this.showMenu()
        } else {
          this.hideMenu()
        }
      }
    },
    disabled(newValue, oldValue) {
      if (newValue !== oldValue && newValue && this.visible) {
        // Hide dropdown if disabled changes to true
        this.visible = false
      }
    }
  },
  created() {
    // Create private non-reactive props
    this.$_popper = null
    this.$_hideTimeout = null
  },
  /* istanbul ignore next */
  deactivated() {
    // In case we are inside a `<keep-alive>`
    this.visible = false
    this.whileOpenListen(false)
    this.destroyPopper()
  },
  beforeDestroy() {
    this.visible = false
    this.whileOpenListen(false)
    this.destroyPopper()
    this.clearHideTimeout()
  },
  methods: {
    // Event emitter
    emitEvent(bvEvent) {
      const { type } = bvEvent
      this.emitOnRoot(getRootEventName(NAME_DROPDOWN, type), bvEvent)
      this.$emit(type, bvEvent)
    },
    showMenu() {
      if (this.disabled) {
        /* istanbul ignore next */
        return
      }

      // Only instantiate Popper.js when dropdown is not in `<b-navbar>`
      if (!this.inNavbar) {
        if (typeof Popper === 'undefined') {
          /* istanbul ignore next */
          warn('Popper.js not found. Falling back to CSS positioning', NAME_DROPDOWN)
        } else {
          // For dropup with alignment we use the parent element as popper container
          let el = (this.dropup && this.right) || this.split ? this.$el : this.$refs.toggle
          // Make sure we have a reference to an element, not a component!
          el = el.$el || el
          // Instantiate Popper.js
          this.createPopper(el)
        }
      }

      // Ensure other menus are closed
      this.emitOnRoot(ROOT_EVENT_NAME_SHOWN, this)

      // Enable listeners
      this.whileOpenListen(true)

      // Wrap in `$nextTick()` to ensure menu is fully rendered/shown
      this.$nextTick(() => {
        // Focus on the menu container on show
        this.focusMenu()
        // Emit the shown event
        this.$emit(EVENT_NAME_SHOWN)
      })
    },
    hideMenu() {
      this.whileOpenListen(false)
      this.emitOnRoot(ROOT_EVENT_NAME_HIDDEN, this)
      this.$emit(EVENT_NAME_HIDDEN)
      this.destroyPopper()
    },
    createPopper(element) {
      this.destroyPopper()
      this.$_popper = new Popper(element, this.$refs.menu, this.getPopperConfig())
    },
    // Ensure popper event listeners are removed cleanly
    destroyPopper() {
      this.$_popper && this.$_popper.destroy()
      this.$_popper = null
    },
    // Instructs popper to re-computes the dropdown position
    // useful if the content changes size
    updatePopper() {
      try {
        this.$_popper.scheduleUpdate()
      } catch {}
    },
    clearHideTimeout() {
      clearTimeout(this.$_hideTimeout)
      this.$_hideTimeout = null
    },
    getPopperConfig() {
      let placement = PLACEMENT_BOTTOM_START
      if (this.dropup) {
        placement = this.right ? PLACEMENT_TOP_END : PLACEMENT_TOP_START
      } else if (this.dropright) {
        placement = PLACEMENT_RIGHT_START
      } else if (this.dropleft) {
        placement = PLACEMENT_LEFT_START
      } else if (this.right) {
        placement = PLACEMENT_BOTTOM_END
      }
      const popperConfig = {
        placement,
        modifiers: {
          offset: { offset: this.offset || 0 },
          flip: { enabled: !this.noFlip }
        }
      }
      const boundariesElement = this.boundary
      if (boundariesElement) {
        popperConfig.modifiers.preventOverflow = { boundariesElement }
      }
      return mergeDeep(popperConfig, this.popperOpts || {})
    },
    // Turn listeners on/off while open
    whileOpenListen(isOpen) {
      // Hide the dropdown when clicked outside
      this.listenForClickOut = isOpen
      // Hide the dropdown when it loses focus
      this.listenForFocusIn = isOpen
      // Hide the dropdown when another dropdown is opened
      const method = isOpen ? '$on' : '$off'
      this.$root[method](ROOT_EVENT_NAME_SHOWN, this.rootCloseListener)
    },
    rootCloseListener(vm) {
      if (vm !== this) {
        this.visible = false
      }
    },
    // Public method to show dropdown
    show() {
      if (this.disabled) {
        return
      }
      // Wrap in a `requestAF()` to allow any previous
      // click handling to occur first
      requestAF(() => {
        this.visible = true
      })
    },
    // Public method to hide dropdown
    hide(refocus = false) {
      /* istanbul ignore next */
      if (this.disabled) {
        return
      }
      this.visible = false
      if (refocus) {
        // Child element is closing the dropdown on click
        this.$once(EVENT_NAME_HIDDEN, this.focusToggler)
      }
    },
    // Called only by a button that toggles the menu
    toggle(event) {
      event = event || {}
      // Early exit when not a click event or ENTER, SPACE or DOWN were pressed
      const { type, keyCode } = event
      if (
        type !== 'click' &&
        !(type === 'keydown' && [CODE_ENTER, CODE_SPACE, CODE_DOWN].indexOf(keyCode) !== -1)
      ) {
        /* istanbul ignore next */
        return
      }
      /* istanbul ignore next */
      if (this.disabled) {
        this.visible = false
        return
      }
      this.$emit(EVENT_NAME_TOGGLE, event)
      stopEvent(event)
      // Toggle visibility
      if (this.visible) {
        this.hide(true)
      } else {
        this.show()
      }
    },
    // Mousedown handler for the toggle
    /* istanbul ignore next */
    onMousedown(event) {
      // We prevent the 'mousedown' event for the toggle to stop the
      // 'focusin' event from being fired
      // The event would otherwise be picked up by the global 'focusin'
      // listener and there is no cross-browser solution to detect it
      // relates to the toggle click
      // The 'click' event will still be fired and we handle closing
      // other dropdowns there too
      // See https://github.com/bootstrap-vue/bootstrap-vue/issues/4328
      stopEvent(event, { propagation: false })
    },
    // Called from dropdown menu context
    onKeydown(event) {
      const { keyCode } = event
      if (keyCode === CODE_ESC) {
        // Close on ESC
        this.onEsc(event)
      } else if (keyCode === CODE_DOWN) {
        // Down Arrow
        this.focusNext(event, false)
      } else if (keyCode === CODE_UP) {
        // Up Arrow
        this.focusNext(event, true)
      }
    },
    // If user presses ESC, close the menu
    onEsc(event) {
      if (this.visible) {
        this.visible = false
        stopEvent(event)
        // Return focus to original trigger button
        this.$once(EVENT_NAME_HIDDEN, this.focusToggler)
      }
    },
    // Called only in split button mode, for the split button
    onSplitClick(event) {
      /* istanbul ignore next */
      if (this.disabled) {
        this.visible = false
        return
      }
      this.$emit(EVENT_NAME_CLICK, event)
    },
    // Shared hide handler between click-out and focus-in events
    hideHandler(event) {
      const { target } = event
      if (this.visible && !contains(this.$refs.menu, target) && !contains(this.toggler, target)) {
        this.clearHideTimeout()
        this.$_hideTimeout = setTimeout(() => this.hide(), this.hideDelay)
      }
    },
    // Document click-out listener
    clickOutHandler(event) {
      this.hideHandler(event)
    },
    // Document focus-in listener
    focusInHandler(event) {
      this.hideHandler(event)
    },
    // Keyboard nav
    focusNext(event, up) {
      // Ignore key up/down on form elements
      const { target } = event
      if (!this.visible || (event && closest(SELECTOR_FORM_CHILD, target))) {
        /* istanbul ignore next: should never happen */
        return
      }
      stopEvent(event)
      this.$nextTick(() => {
        const items = this.getItems()
        if (items.length < 1) {
          /* istanbul ignore next: should never happen */
          return
        }
        let index = items.indexOf(target)
        if (up && index > 0) {
          index--
        } else if (!up && index < items.length - 1) {
          index++
        }
        if (index < 0) {
          /* istanbul ignore next: should never happen */
          index = 0
        }
        this.focusItem(index, items)
      })
    },
    focusItem(index, items) {
      const el = items.find((el, i) => i === index)
      attemptFocus(el)
    },
    getItems() {
      // Get all items
      return filterVisibles(selectAll(SELECTOR_ITEM, this.$refs.menu))
    },
    focusMenu() {
      attemptFocus(this.$refs.menu)
    },
    focusToggler() {
      this.$nextTick(() => {
        attemptFocus(this.toggler)
      })
    }
  }
})
<|endoftext|>


<commit_before>
const stopThresholdDefault = 0.3;
const bounceDeceleration = 0.04;
const bounceAcceleration = 0.11;

export default class Impetus {
	constructor({
		source: sourceEl = document,
		update: updateCallback,
		multiplier = 1,
		friction = 0.92,
		initialValues,
		boundX,
		boundY,
		bounce = true
	}) {
		var boundXmin, boundXmax, boundYmin, boundYmax, pointerLastX, pointerLastY, pointerCurrentX, pointerCurrentY, pointerId, decVelX, decVelY;
		var targetX = 0;
		var targetY = 0;
		var stopThreshold = stopThresholdDefault * multiplier;
		var ticking = false;
		var pointerActive = false;
		var paused = false;
		var decelerating = false;
		var trackingPoints = [];

		/**
		 * Initialize instance
		 */
		(function init() {
			sourceEl = (typeof sourceEl === 'string') ? document.querySelector(sourceEl) : sourceEl;
			if (!sourceEl) {
				throw new Error('IMPETUS: source not found.');
			}

			if (!updateCallback) {
				throw new Error('IMPETUS: update function not defined.');
			}

			if (initialValues) {
				if (initialValues[0]) {
					targetX = initialValues[0];
				}
				if (initialValues[1]) {
					targetY = initialValues[1];
				}
				callUpdateCallback();
			}

			// Initialize bound values
			if (boundX) {
				boundXmin = boundX[0];
				boundXmax = boundX[1];
			}
			if (boundY) {
				boundYmin = boundY[0];
				boundYmax = boundY[1];
			}

			sourceEl.addEventListener('touchstart', onDown);
			sourceEl.addEventListener('mousedown', onDown);
		})();

		/**
		 * In edge cases where you may need to
		 * reinstanciate Impetus on the same sourceEl
		 * this will remove the previous event listeners
		 */
		this.destroy = function() {
			sourceEl.removeEventListener('touchstart', onDown);
			sourceEl.removeEventListener('mousedown', onDown);
			// however it won't "destroy" a reference
			// to instance if you'd like to do that
			// it returns null as a convinience.
			// ex: `instance = instance.destroy();`
			return null;
		};

		/**
		 * Disable movement processing
		 * @public
		 */
		this.pause = function() {
			pointerActive = false;
			paused = true;
		};

		/**
		 * Enable movement processing
		 * @public
		 */
		this.resume = function() {
			paused = false;
		};

		/**
		 * Update the current x and y values
		 * @public
		 * @param {Number} x
		 * @param {Number} y
		 */
		this.setValues = function(x, y) {
			if (typeof x === 'number') {
				targetX = x;
			}
			if (typeof y === 'number') {
				targetY = y;
			}
		};

		/**
		 * Update the multiplier value
		 * @public
		 * @param {Number} val
		 */
		this.setMultiplier = function(val) {
			multiplier = val;
			stopThreshold = stopThresholdDefault * multiplier;
		};

		/**
		 * Update boundX value
		 * @public
		 * @param {Number[]} boundX
		 */
		this.setBoundX = function(boundX) {
			boundXmin = boundX[0];
			boundXmax = boundX[1];
		};

		/**
		 * Update boundY value
		 * @public
		 * @param {Number[]} boundY
		 */
		this.setBoundY = function(boundY) {
			boundYmin = boundY[0];
			boundYmax = boundY[1];
		};

		/**
		 * Executes the update function
		 */
		function callUpdateCallback() {
			updateCallback.call(sourceEl, targetX, targetY);
		}

		/**
		 * Creates a custom normalized event object from touch and mouse events
		 * @param  {Event} ev
		 * @returns {Object} with x, y, and id properties
		 */
		function normalizeEvent(ev) {
			if (ev.type === 'touchmove' || ev.type === 'touchstart' || ev.type === 'touchend') {
				var touch = ev.targetTouches[0] || ev.changedTouches[0];
				return {
					x: touch.clientX,
					y: touch.clientY,
					id: touch.identifier
				};
			} else { // mouse events
				return {
					x: ev.clientX,
					y: ev.clientY,
					id: null
				};
			}
		}

		/**
		 * Initializes movement tracking
		 * @param  {Object} ev Normalized event
		 */
		function onDown(ev) {
			var event = normalizeEvent(ev);
			if (!pointerActive && !paused) {
				pointerActive = true;
				decelerating = false;
				pointerId = event.id;

				pointerLastX = pointerCurrentX = event.x;
				pointerLastY = pointerCurrentY = event.y;
				trackingPoints = [];
				addTrackingPoint(pointerLastX, pointerLastY);

				document.addEventListener('touchmove', onMove);
				document.addEventListener('touchend', onUp);
				document.addEventListener('touchcancel', stopTracking);
				document.addEventListener('mousemove', onMove);
				document.addEventListener('mouseup', onUp);
				document.addEventListener('mouseout', onUp);
			}
		}

		/**
		 * Handles move events
		 * @param  {Object} ev Normalized event
		 */
		function onMove(ev) {
			ev.preventDefault();
			var event = normalizeEvent(ev);

			if (pointerActive && event.id === pointerId) {
				pointerCurrentX = event.x;
				pointerCurrentY = event.y;
				addTrackingPoint(pointerLastX, pointerLastY);
				requestTick();
			}
		}

		/**
		 * Handles up/end events
		 * @param {Object} ev Normalized event
		 */
		function onUp(ev) {
			var event = normalizeEvent(ev);

			if (pointerActive && event.id === pointerId) {
				stopTracking();
			}
		}

		/**
		 * Stops movement tracking, starts animation
		 */
		function stopTracking() {
			pointerActive = false;
			addTrackingPoint(pointerLastX, pointerLastY);
			startDecelAnim();

			document.removeEventListener('touchmove', onMove);
			document.removeEventListener('touchend', onUp);
			document.removeEventListener('touchcancel', stopTracking);
			document.removeEventListener('mouseup', onUp);
			document.removeEventListener('mouseout', onUp);
			document.removeEventListener('mousemove', onMove);
		}

		/**
		 * Records movement for the last 100ms
		 * @param {number} x
		 * @param {number} y [description]
		 */
		function addTrackingPoint(x, y) {
			var time = Date.now();
			while (trackingPoints.length > 0) {
				if (time - trackingPoints[0].time <= 100) {
					break;
				}
				trackingPoints.shift();
			}

			trackingPoints.push({x, y, time});
		}

		/**
		 * Calculate new values, call update function
		 */
		function updateAndRender() {
			var pointerChangeX = pointerCurrentX - pointerLastX;
			var pointerChangeY = pointerCurrentY - pointerLastY;

			targetX += pointerChangeX * multiplier;
			targetY += pointerChangeY * multiplier;

			if (bounce) {
				let diff = checkBounds();
				if (diff.x !== 0) {
					targetX -= pointerChangeX * dragOutOfBoundsMultiplier(diff.x) * multiplier;
				}
				if (diff.y !== 0) {
					targetY -= pointerChangeY * dragOutOfBoundsMultiplier(diff.y) * multiplier;
				}
			} else {
				checkBounds(true);
			}

			callUpdateCallback();

			pointerLastX = pointerCurrentX;
			pointerLastY = pointerCurrentY;
			ticking = false;
		}

		/**
		 * Returns a value from around 0.5 to 1, based on distance
		 * @param {Number} val
		 */
		function dragOutOfBoundsMultiplier(val) {
			return 0.000005 * Math.pow(val, 2) + 0.0001 * val + 0.55;
		}

		/**
		 * prevents animating faster than current framerate
		 */
		function requestTick() {
			if (!ticking) {
				requestAnimFrame(updateAndRender);
			}
			ticking = true;
		}

		/**
		 * Determine position relative to bounds
		 * @param {Boolean} restrict Whether to restrict target to bounds
		 */
		function checkBounds(restrict) {
			var xDiff = 0;
			var yDiff = 0;

			if (boundXmin !== undefined && targetX < boundXmin) {
				xDiff = boundXmin - targetX;
			} else if (boundXmax !== undefined && targetX > boundXmax) {
				xDiff = boundXmax - targetX;
			}

			if (boundYmin !== undefined && targetY < boundYmin) {
				yDiff = boundYmin - targetY;
			} else if (boundYmax !== undefined && targetY > boundYmax) {
				yDiff = boundYmax - targetY;
			}

			if (restrict) {
				if (xDiff !== 0) {
					targetX = (xDiff > 0) ? boundXmin : boundXmax;
				}
				if (yDiff !== 0) {
					targetY = (yDiff > 0) ? boundYmin : boundYmax;
				}
			}

			return {
				x: xDiff,
				y: yDiff,
				inBounds: xDiff === 0 && yDiff === 0
			};
		}

		/**
		 * Initialize animation of values coming to a stop
		 */
		function startDecelAnim() {
			var firstPoint = trackingPoints[0];
			var lastPoint = trackingPoints[trackingPoints.length - 1];

			var xOffset = lastPoint.x - firstPoint.x;
			var yOffset = lastPoint.y - firstPoint.y;
			var timeOffset = lastPoint.time - firstPoint.time;

			var D = (timeOffset / 15) / multiplier;

			decVelX = (xOffset / D) || 0; // prevent NaN
			decVelY = (yOffset / D) || 0;

			var diff = checkBounds();

			if ((Math.abs(decVelX) > 1 || Math.abs(decVelY) > 1) || !diff.inBounds){
				decelerating = true;
				requestAnimFrame(stepDecelAnim);
			}
		}

		/**
		 * Animates values slowing down
		 */
		function stepDecelAnim() {
			if (!decelerating) {
				return;
			}

			decVelX *= friction;
			decVelY *= friction;

			targetX += decVelX;
			targetY += decVelY;

			var diff = checkBounds();

			if ((Math.abs(decVelX) > stopThreshold || Math.abs(decVelY) > stopThreshold) || !diff.inBounds) {

				if (bounce) {
					let reboundAdjust = 2.5;

					if (diff.x !== 0) {
						if (diff.x * decVelX <= 0) {
							decVelX += diff.x * bounceDeceleration;
						} else {
							let adjust = (diff.x > 0) ? reboundAdjust : -reboundAdjust;
							decVelX = (diff.x + adjust) * bounceAcceleration;
						}
					}
					if (diff.y !== 0) {
						if (diff.y * decVelY <= 0) {
							decVelY += diff.y * bounceDeceleration;
						} else {
							let adjust = (diff.y > 0) ? reboundAdjust : -reboundAdjust;
							decVelY = (diff.y + adjust) * bounceAcceleration;
						}
					}
				} else {
					if (diff.x !== 0) {
						if (diff.x > 0) {
							targetX = boundXmin;
						} else {
							targetX = boundXmax;
						}
						decVelX = 0;
					}
					if (diff.y !== 0) {
						if (diff.y > 0) {
							targetY = boundYmin;
						} else {
							targetY = boundYmax;
						}
						decVelY = 0;
					}
				}

				callUpdateCallback();

				requestAnimFrame(stepDecelAnim);
			} else {
				decelerating = false;
			}
		}
	}
}

/**
 * @see http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
 */
const requestAnimFrame = (function(){
	return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) {
		window.setTimeout(callback, 1000 / 60);
	};
})();
<commit_msg>fix: preventDefault not working in Safari 10<commit_after>
const stopThresholdDefault = 0.3;
const bounceDeceleration = 0.04;
const bounceAcceleration = 0.11;

// fixes weird safari 10 bug where preventDefault is prevented
// @see https://github.com/metafizzy/flickity/issues/457#issuecomment-254501356
window.addEventListener('touchmove', function() {});

export default class Impetus {
	constructor({
		source: sourceEl = document,
		update: updateCallback,
		multiplier = 1,
		friction = 0.92,
		initialValues,
		boundX,
		boundY,
		bounce = true
	}) {
		var boundXmin, boundXmax, boundYmin, boundYmax, pointerLastX, pointerLastY, pointerCurrentX, pointerCurrentY, pointerId, decVelX, decVelY;
		var targetX = 0;
		var targetY = 0;
		var stopThreshold = stopThresholdDefault * multiplier;
		var ticking = false;
		var pointerActive = false;
		var paused = false;
		var decelerating = false;
		var trackingPoints = [];

		/**
		 * Initialize instance
		 */
		(function init() {
			sourceEl = (typeof sourceEl === 'string') ? document.querySelector(sourceEl) : sourceEl;
			if (!sourceEl) {
				throw new Error('IMPETUS: source not found.');
			}

			if (!updateCallback) {
				throw new Error('IMPETUS: update function not defined.');
			}

			if (initialValues) {
				if (initialValues[0]) {
					targetX = initialValues[0];
				}
				if (initialValues[1]) {
					targetY = initialValues[1];
				}
				callUpdateCallback();
			}

			// Initialize bound values
			if (boundX) {
				boundXmin = boundX[0];
				boundXmax = boundX[1];
			}
			if (boundY) {
				boundYmin = boundY[0];
				boundYmax = boundY[1];
			}

			sourceEl.addEventListener('touchstart', onDown);
			sourceEl.addEventListener('mousedown', onDown);
		})();

		/**
		 * In edge cases where you may need to
		 * reinstanciate Impetus on the same sourceEl
		 * this will remove the previous event listeners
		 */
		this.destroy = function() {
			sourceEl.removeEventListener('touchstart', onDown);
			sourceEl.removeEventListener('mousedown', onDown);
			// however it won't "destroy" a reference
			// to instance if you'd like to do that
			// it returns null as a convinience.
			// ex: `instance = instance.destroy();`
			return null;
		};

		/**
		 * Disable movement processing
		 * @public
		 */
		this.pause = function() {
			pointerActive = false;
			paused = true;
		};

		/**
		 * Enable movement processing
		 * @public
		 */
		this.resume = function() {
			paused = false;
		};

		/**
		 * Update the current x and y values
		 * @public
		 * @param {Number} x
		 * @param {Number} y
		 */
		this.setValues = function(x, y) {
			if (typeof x === 'number') {
				targetX = x;
			}
			if (typeof y === 'number') {
				targetY = y;
			}
		};

		/**
		 * Update the multiplier value
		 * @public
		 * @param {Number} val
		 */
		this.setMultiplier = function(val) {
			multiplier = val;
			stopThreshold = stopThresholdDefault * multiplier;
		};

		/**
		 * Update boundX value
		 * @public
		 * @param {Number[]} boundX
		 */
		this.setBoundX = function(boundX) {
			boundXmin = boundX[0];
			boundXmax = boundX[1];
		};

		/**
		 * Update boundY value
		 * @public
		 * @param {Number[]} boundY
		 */
		this.setBoundY = function(boundY) {
			boundYmin = boundY[0];
			boundYmax = boundY[1];
		};

		/**
		 * Executes the update function
		 */
		function callUpdateCallback() {
			updateCallback.call(sourceEl, targetX, targetY);
		}

		/**
		 * Creates a custom normalized event object from touch and mouse events
		 * @param  {Event} ev
		 * @returns {Object} with x, y, and id properties
		 */
		function normalizeEvent(ev) {
			if (ev.type === 'touchmove' || ev.type === 'touchstart' || ev.type === 'touchend') {
				var touch = ev.targetTouches[0] || ev.changedTouches[0];
				return {
					x: touch.clientX,
					y: touch.clientY,
					id: touch.identifier
				};
			} else { // mouse events
				return {
					x: ev.clientX,
					y: ev.clientY,
					id: null
				};
			}
		}

		/**
		 * Initializes movement tracking
		 * @param  {Object} ev Normalized event
		 */
		function onDown(ev) {
			var event = normalizeEvent(ev);
			if (!pointerActive && !paused) {
				pointerActive = true;
				decelerating = false;
				pointerId = event.id;

				pointerLastX = pointerCurrentX = event.x;
				pointerLastY = pointerCurrentY = event.y;
				trackingPoints = [];
				addTrackingPoint(pointerLastX, pointerLastY);

				document.addEventListener('touchmove', onMove);
				document.addEventListener('touchend', onUp);
				document.addEventListener('touchcancel', stopTracking);
				document.addEventListener('mousemove', onMove);
				document.addEventListener('mouseup', onUp);
				document.addEventListener('mouseout', onUp);
			}
		}

		/**
		 * Handles move events
		 * @param  {Object} ev Normalized event
		 */
		function onMove(ev) {
			ev.preventDefault();
			var event = normalizeEvent(ev);

			if (pointerActive && event.id === pointerId) {
				pointerCurrentX = event.x;
				pointerCurrentY = event.y;
				addTrackingPoint(pointerLastX, pointerLastY);
				requestTick();
			}
		}

		/**
		 * Handles up/end events
		 * @param {Object} ev Normalized event
		 */
		function onUp(ev) {
			var event = normalizeEvent(ev);

			if (pointerActive && event.id === pointerId) {
				stopTracking();
			}
		}

		/**
		 * Stops movement tracking, starts animation
		 */
		function stopTracking() {
			pointerActive = false;
			addTrackingPoint(pointerLastX, pointerLastY);
			startDecelAnim();

			document.removeEventListener('touchmove', onMove);
			document.removeEventListener('touchend', onUp);
			document.removeEventListener('touchcancel', stopTracking);
			document.removeEventListener('mouseup', onUp);
			document.removeEventListener('mouseout', onUp);
			document.removeEventListener('mousemove', onMove);
		}

		/**
		 * Records movement for the last 100ms
		 * @param {number} x
		 * @param {number} y [description]
		 */
		function addTrackingPoint(x, y) {
			var time = Date.now();
			while (trackingPoints.length > 0) {
				if (time - trackingPoints[0].time <= 100) {
					break;
				}
				trackingPoints.shift();
			}

			trackingPoints.push({x, y, time});
		}

		/**
		 * Calculate new values, call update function
		 */
		function updateAndRender() {
			var pointerChangeX = pointerCurrentX - pointerLastX;
			var pointerChangeY = pointerCurrentY - pointerLastY;

			targetX += pointerChangeX * multiplier;
			targetY += pointerChangeY * multiplier;

			if (bounce) {
				let diff = checkBounds();
				if (diff.x !== 0) {
					targetX -= pointerChangeX * dragOutOfBoundsMultiplier(diff.x) * multiplier;
				}
				if (diff.y !== 0) {
					targetY -= pointerChangeY * dragOutOfBoundsMultiplier(diff.y) * multiplier;
				}
			} else {
				checkBounds(true);
			}

			callUpdateCallback();

			pointerLastX = pointerCurrentX;
			pointerLastY = pointerCurrentY;
			ticking = false;
		}

		/**
		 * Returns a value from around 0.5 to 1, based on distance
		 * @param {Number} val
		 */
		function dragOutOfBoundsMultiplier(val) {
			return 0.000005 * Math.pow(val, 2) + 0.0001 * val + 0.55;
		}

		/**
		 * prevents animating faster than current framerate
		 */
		function requestTick() {
			if (!ticking) {
				requestAnimFrame(updateAndRender);
			}
			ticking = true;
		}

		/**
		 * Determine position relative to bounds
		 * @param {Boolean} restrict Whether to restrict target to bounds
		 */
		function checkBounds(restrict) {
			var xDiff = 0;
			var yDiff = 0;

			if (boundXmin !== undefined && targetX < boundXmin) {
				xDiff = boundXmin - targetX;
			} else if (boundXmax !== undefined && targetX > boundXmax) {
				xDiff = boundXmax - targetX;
			}

			if (boundYmin !== undefined && targetY < boundYmin) {
				yDiff = boundYmin - targetY;
			} else if (boundYmax !== undefined && targetY > boundYmax) {
				yDiff = boundYmax - targetY;
			}

			if (restrict) {
				if (xDiff !== 0) {
					targetX = (xDiff > 0) ? boundXmin : boundXmax;
				}
				if (yDiff !== 0) {
					targetY = (yDiff > 0) ? boundYmin : boundYmax;
				}
			}

			return {
				x: xDiff,
				y: yDiff,
				inBounds: xDiff === 0 && yDiff === 0
			};
		}

		/**
		 * Initialize animation of values coming to a stop
		 */
		function startDecelAnim() {
			var firstPoint = trackingPoints[0];
			var lastPoint = trackingPoints[trackingPoints.length - 1];

			var xOffset = lastPoint.x - firstPoint.x;
			var yOffset = lastPoint.y - firstPoint.y;
			var timeOffset = lastPoint.time - firstPoint.time;

			var D = (timeOffset / 15) / multiplier;

			decVelX = (xOffset / D) || 0; // prevent NaN
			decVelY = (yOffset / D) || 0;

			var diff = checkBounds();

			if ((Math.abs(decVelX) > 1 || Math.abs(decVelY) > 1) || !diff.inBounds){
				decelerating = true;
				requestAnimFrame(stepDecelAnim);
			}
		}

		/**
		 * Animates values slowing down
		 */
		function stepDecelAnim() {
			if (!decelerating) {
				return;
			}

			decVelX *= friction;
			decVelY *= friction;

			targetX += decVelX;
			targetY += decVelY;

			var diff = checkBounds();

			if ((Math.abs(decVelX) > stopThreshold || Math.abs(decVelY) > stopThreshold) || !diff.inBounds) {

				if (bounce) {
					let reboundAdjust = 2.5;

					if (diff.x !== 0) {
						if (diff.x * decVelX <= 0) {
							decVelX += diff.x * bounceDeceleration;
						} else {
							let adjust = (diff.x > 0) ? reboundAdjust : -reboundAdjust;
							decVelX = (diff.x + adjust) * bounceAcceleration;
						}
					}
					if (diff.y !== 0) {
						if (diff.y * decVelY <= 0) {
							decVelY += diff.y * bounceDeceleration;
						} else {
							let adjust = (diff.y > 0) ? reboundAdjust : -reboundAdjust;
							decVelY = (diff.y + adjust) * bounceAcceleration;
						}
					}
				} else {
					if (diff.x !== 0) {
						if (diff.x > 0) {
							targetX = boundXmin;
						} else {
							targetX = boundXmax;
						}
						decVelX = 0;
					}
					if (diff.y !== 0) {
						if (diff.y > 0) {
							targetY = boundYmin;
						} else {
							targetY = boundYmax;
						}
						decVelY = 0;
					}
				}

				callUpdateCallback();

				requestAnimFrame(stepDecelAnim);
			} else {
				decelerating = false;
			}
		}
	}
}

/**
 * @see http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
 */
const requestAnimFrame = (function(){
	return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) {
		window.setTimeout(callback, 1000 / 60);
	};
})();
<|endoftext|>


<commit_before>import { Parser } from 'htmlparser2'

export default function(incrementalDom) {
  const {
    attr,
    elementClose,
    elementOpen,
    elementOpenEnd,
    elementOpenStart,
    elementVoid,
    text,
  } = incrementalDom

  const sanitizeName = name => name.replace(/[^-:\w]/g, '')

  const iDOMParser = new Parser(
    {
      onopentag: name => elementOpenEnd(sanitizeName(name)),
      onopentagname: name => elementOpenStart(sanitizeName(name), '', []),
      onattribute: (name, value) => {
        const sanitizedName = sanitizeName(name)
        if (sanitizedName !== '') attr(sanitizedName, value)
      },
      ontext: text,
      onclosetag: name => elementClose(sanitizeName(name)),
    },
    { decodeEntities: true }
  )

  const wrapIncrementalDOM = html =>
    typeof html === 'function' ? html() : iDOMParser.write(html)

  return {
    renderAttrsToArray(token) {
      if (!token.attrs) return []
      return token.attrs.reduce((v, a) => v.concat(a), [])
    },

    renderInline(tokens, options, env) {
      return () => {
        tokens.forEach((current, i) => {
          const { type } = current

          if (typeof this.rules[type] !== 'undefined') {
            wrapIncrementalDOM(this.rules[type](tokens, i, options, env, this))
          } else {
            this.renderToken(tokens, i, options)()
          }
        })
      }
    },

    renderToken(tokens, idx) {
      return () => {
        const token = tokens[idx]
        if (token.hidden) return

        if (token.nesting === 0) {
          elementVoid(token.tag, '', [], ...this.renderAttrsToArray(token))
        } else if (token.nesting === -1) {
          elementClose(token.tag)
        } else {
          elementOpen(token.tag, '', [], ...this.renderAttrsToArray(token))
        }
      }
    },

    render(tokens, options, env) {
      return () => {
        tokens.forEach((current, i) => {
          const { type } = current

          if (type === 'inline') {
            this.renderInline(current.children, options, env)()
          } else if (typeof this.rules[type] !== 'undefined') {
            wrapIncrementalDOM(this.rules[type](tokens, i, options, env, this))
          } else {
            this.renderToken(tokens, i, options, env)()
          }
        })
        iDOMParser.end()
        iDOMParser.reset()
      }
    },
  }
}
<commit_msg>Import Parser of htmlparser2 directly to reduce browser bundle size<commit_after>import Parser from 'htmlparser2/lib/Parser'

export default function(incrementalDom) {
  const {
    attr,
    elementClose,
    elementOpen,
    elementOpenEnd,
    elementOpenStart,
    elementVoid,
    text,
  } = incrementalDom

  const sanitizeName = name => name.replace(/[^-:\w]/g, '')

  const iDOMParser = new Parser(
    {
      onopentag: name => elementOpenEnd(sanitizeName(name)),
      onopentagname: name => elementOpenStart(sanitizeName(name), '', []),
      onattribute: (name, value) => {
        const sanitizedName = sanitizeName(name)
        if (sanitizedName !== '') attr(sanitizedName, value)
      },
      ontext: text,
      onclosetag: name => elementClose(sanitizeName(name)),
    },
    { decodeEntities: true }
  )

  const wrapIncrementalDOM = html =>
    typeof html === 'function' ? html() : iDOMParser.write(html)

  return {
    renderAttrsToArray(token) {
      if (!token.attrs) return []
      return token.attrs.reduce((v, a) => v.concat(a), [])
    },

    renderInline(tokens, options, env) {
      return () => {
        tokens.forEach((current, i) => {
          const { type } = current

          if (typeof this.rules[type] !== 'undefined') {
            wrapIncrementalDOM(this.rules[type](tokens, i, options, env, this))
          } else {
            this.renderToken(tokens, i, options)()
          }
        })
      }
    },

    renderToken(tokens, idx) {
      return () => {
        const token = tokens[idx]
        if (token.hidden) return

        if (token.nesting === 0) {
          elementVoid(token.tag, '', [], ...this.renderAttrsToArray(token))
        } else if (token.nesting === -1) {
          elementClose(token.tag)
        } else {
          elementOpen(token.tag, '', [], ...this.renderAttrsToArray(token))
        }
      }
    },

    render(tokens, options, env) {
      return () => {
        tokens.forEach((current, i) => {
          const { type } = current

          if (type === 'inline') {
            this.renderInline(current.children, options, env)()
          } else if (typeof this.rules[type] !== 'undefined') {
            wrapIncrementalDOM(this.rules[type](tokens, i, options, env, this))
          } else {
            this.renderToken(tokens, i, options, env)()
          }
        })
        iDOMParser.end()
        iDOMParser.reset()
      }
    },
  }
}
<|endoftext|>