<commit_before>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
/*global Config, $, jQuery, get_cookie, delete_cookie, set_cookie, window, alert */

var sakai = sakai || {};

/**
 * @name sakai.topnavigation
 *
 * @class topnavigation
 *
 * @description
 * Initialize the topnavigation widget
 *
 * @version 0.0.1
 * @param {String} tuid Unique id of the widget
 * @param {Boolean} showSettings Show the settings of the widget or not
 */
sakai.topnavigation = function(tuid, showSettings){

    /////////////////////////////
    // Configuration variables //
    /////////////////////////////

    var currentChatStatus = "";

    // Links and labels
    var hiLabel = "#hispan";
    var myprofileName = "#myprofile_name";
    var onlineButton = "#online_button";
    var pictureHolder = "#picture_holder";
    var showOnlineLink = "#show_online";
    var userIdLabel = "#userid";

    // User Link
    var userLink = "#user_link";
    var userLinkMenu = userLink + "_menu";
    var userLinkMenuLink = userLink + "_menu" + " a";

    // Navigation
    var nav = "#nav";
    var navContentMediaLink = "#nav_content_media_link";
    var navCoursesSitesLink = "#nav_courses_sites_link";
    var navCoursesSitesLinkClass = "#nav_courses_sites_link";
    var navPeopleLink = "#nav_people_link";
    var navPeopleLinkClass = "nav_people_link";
    var navMySakaiLink = "#nav_my_sakai_link";
    var navCalendarLink = "#nav_calendar_link";
    var navSelectedNavItemClass = "explore_nav_selected";

    // Messages
    var chatUnreadMessages = "#chat_unreadMessages";

    // Search
    var $general_search_form = $("#general_search_container form");
    var $general_search_input = $("#general_search_input");
    var $general_search_default_value = $("#general_search_default_value");
    var searchFocus = false;

    // Containers
    var exploreNavigationContainer = "#explore_nav_container";

    // CSS Classes
    var searchInputFocusClass = "search_input_focus";
    var chatAvailableStatusClass = "chat_available_status";
    var chatAvailableStatusClassOnline = chatAvailableStatusClass + "_online";
    var chatAvailableStatusClassBusy = chatAvailableStatusClass + "_busy";
    var chatAvailableStatusClassOffline = chatAvailableStatusClass + "_offline";

    var userLinkChatStatusClass = ".user_link_chat_status";

    ///////////////////////
    // Utility functions //
    ///////////////////////

    /**
     * Get the number of messages that are unread and show it.
     */
    var getCountUnreadMessages = function(){
        //we have the number of unread messages as a part of the me-feed
        //so get it directly from me object.
        $(chatUnreadMessages).text(sakai.data.me.messages.unread);
    };

    /**
     * Update a certain element with a specific chatstatus
     * @param {Object} element Element that needs to be updated
     * @param {String} chatstatus The chatstatus that needs to be added
     */
    var updateChatStatusElement = function(element, chatstatus){
        element.removeClass(chatAvailableStatusClassOnline);
        element.removeClass(chatAvailableStatusClassBusy);
        element.removeClass(chatAvailableStatusClassOffline);
        element.addClass(chatAvailableStatusClass + "_" + chatstatus);
    };

    /**
     * Update the status on the page by firing an event that handles this
     */
    var updateChatStatus = function(){
        // Trigger the chat_status_change event to update other widgets
        $(window).trigger("chat_status_change", currentChatStatus);
    };

    /**
     * Set the chatstatus of the user
     * @param {String} chatstatus The chatstatus which should be
     * online/offline or busy
     */
    var sendChatStatus = function(chatstatus){
        currentChatStatus = chatstatus;

        var data = {
            "chatstatus": chatstatus,
            "_charset_": "utf-8"
        };

        $.ajax({
            url: sakai.data.me.profile["jcr:path"],
            type: "POST",
            data: data,
            success: function(data){
                updateChatStatus();
            },
            error: function(xhr, textStatus, thrownError){
                alert("An error occurend when sending the status to the server.");
            }
        });
    };

    /**
     * Show or hide the user link menu
     * @param {Boolean} hideOnly
     *  true: Hide the menu only
     *  false: Show or hide the menu depending if it's already visible
     */
    var showHideUserLinkMenu = function(hideOnly){
        if ($(userLinkMenu).is(":visible") || hideOnly) {
            $(userLinkMenu).hide();
        }
        else {
            $(userLinkMenu).css("left", Math.round($(userLink).offset().left) + "px");
            $(userLinkMenu).css("top", (Math.round($(userLink).offset().top) + $(userLink).height() + 2) + "px");
            $(userLinkMenu).css("width", ($(userLink).width() + 10) + "px");
            $(userLinkMenu).show();
        }
    };

    /**
     * Add binding to some elements
     */
    var addBinding = function(){
        $(userLink).bind("click", function(){
            showHideUserLinkMenu(false);
        });

        $(userLinkChatStatusClass).bind("click", function(ev){
            showHideUserLinkMenu(false);
            var clicked = ev.currentTarget.id.split("_")[ev.currentTarget.id.split("_").length - 1];
            sendChatStatus(clicked);
        });

        $(document).bind("click", function(e){
            var $clicked = $(e.target);

            // Check if one of the parents is the userLink
            if (!$clicked.parents().is(userLink)) {
                showHideUserLinkMenu(true);
            }
        });

        $(window).bind("chat_status_change", function(event, currentChatStatus){
            updateChatStatusElement($(userLink), currentChatStatus)
        });
    };

    ////////////////
    // NAVIGATION //
    ///////////////

    /**
     * Select the page in the top navigation where you are currently on.
     * This will apply a class to the selected navigation item based on the current URL
     * @returns void;
     */
    var determineCurrentNav = function(){
        var windowLocationPath = window.location.pathname.toLowerCase();

        // Remove all selected classes from nav elements
        $(nav + " " + navSelectedNavItemClass).removeClass(navSelectedNavItemClass);

        // My Sakai
        if ((windowLocationPath.indexOf(sakai.config.URL.MY_DASHBOARD_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_MY_DASHBOARD_URL) !== -1)) {
            $(navMySakaiLink).addClass(navSelectedNavItemClass);
            return;
        }

        // Content & Media
        if ((windowLocationPath.indexOf(sakai.config.URL.CONTENT_MEDIA_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_CONTENT_MEDIA_URL) !== -1)) {
            $(navContentMediaLink).addClass(navSelectedNavItemClass);
            return;
        }

        // People
        if ((windowLocationPath.indexOf(sakai.config.URL.PEOPLE_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_PEOPLE_URL) !== -1)) {
            $(navPeopleLink).addClass(navSelectedNavItemClass);
            return;
        }

        // Courses & Sites
        if ((windowLocationPath.indexOf(sakai.config.URL.COURSES_SITES_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_COURSES_SITES_URL) !== -1) || (windowLocationPath.indexOf("/sites/") !== -1)) {
            $(navCoursesSitesLink).addClass(navSelectedNavItemClass);
            return;
        }

        // Calendar
        if ((windowLocationPath.indexOf(sakai.config.URL.SEARCH_GENERAL_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.SEARCH_PEOPLE_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.SEARCH_SITES_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.SEARCH_CONTENT_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_SEARCH_URL) !== -1)) {
            $(navCalendarLink).addClass(navSelectedNavItemClass);
            return;
        }

    };

    ////////////
    // SEARCH //
    ////////////

    /**
     * Execute the search for the value that is in the search input field
     */
    var doSearch = function(){
        var tosearch = $general_search_input.val();
        // Disable search button
        $("#general_search_submit_button").attr("disabled", true);
        if (tosearch) {
            // Redirecting back to the general search page. This expects the URL to be
            // in a format like this one: page.html#pageid|searchstring
            document.location = sakai.config.URL.SEARCH_GENERAL_URL + "#q=" + tosearch;
            // Only enable button if the location is the search page
            if (window.location.pathname.split("/")[2] === "search.html") {
                $("#general_search_submit_button").attr("disabled", false);
            }
        }
    };

    /**
     * If this is the first time the field gets focus, we'll make his text color black
     * and remove the default value
     */
    $general_search_input.bind("focus", function(ev){
        if (!searchFocus) {
            $general_search_input.val("").addClass(searchInputFocusClass);
            searchFocus = true;
        }
    });

    /**
     * If we leave the field without filling out anything, we'll reinsert the default
     * search inputbox value
     */
    $general_search_input.bind("blur", function(ev){

        if (!$general_search_input.val()) {
            $general_search_input.removeClass(searchInputFocusClass);
            $general_search_input.val($general_search_default_value.text());
            searchFocus = false;
        }
    });

    /**
     * Bind the submit event to the search form
     * This event is triggered when you hit enter in the input field and
     * when you click on the submit button
     */
    $general_search_form.bind("submit", function(){

        doSearch();
        return false;
    });

    /**
     * Switch navigation bar to anonymous mode
     * @returns void
     */
    var switchToAnonymousMode = function(){

        // Show Nav Container
        $(exploreNavigationContainer).show();

        // Hide things which are irrelvant for Anonymous user
        $(".personal .mail").hide();
        $(".personal .sign_out").hide();
        $("#user_link_container").hide();

        // Show anonymous elements
        $("#other_logins_button_container").show();
        $("#register_button_container").show();
        $("#login_button_container").show();

        // Set up public nav links
        $("#nav_my_sakai_link a").attr("href", sakai.config.URL.PUBLIC_MY_DASHBOARD_URL);
        $("#nav_content_media_link a").attr("href", sakai.config.URL.PUBLIC_CONTENT_MEDIA_URL_PAGE);
        $("#nav_people_link a").attr("href", sakai.config.URL.PUBLIC_PEOPLE_URL);
        $("#nav_courses_sites_link a").attr("href", sakai.config.URL.PUBLIC_COURSES_SITES_URL);
        $("#nav_search_link a").attr("href", sakai.config.URL.PUBLIC_SEARCH_URL_PAGE);

        // Make the login page redirect to the current page after login
        $(".log_in").attr("href", $(".log_in").attr("href") + "?url=" + escape(window.location.pathname + window.location.search + window.location.hash));

    };

    /**
     * Set the presence for the current user
     * We need to do this in order to let the server know the user is still available for chatting.
     */
    var setPresence = function(){

        var data = {
            "sakai:status": "online",
            "_charset_": "utf-8"
        };

        $.ajax({
            url: sakai.config.URL.PRESENCE_SERVICE,
            type: "POST",
            success: function(){
                setTimeout(setPresence, 120000);
            },
            data: data
        });
    };

    /**
     * Parse the chatstatus for a user
     * @param {String} chatStatus The chatstatus which should be
     * online, busy or offline
     */
    var parseChatStatus = function(chatStatus){
        // Check if the status is defined
        if (!chatStatus) {
            chatStatus = "online";
        }
        return chatStatus;
    };

    /**
     * Get the chat status for the current user
     */
    var getChatStatus = function(){
        if (sakai.data.me.profile) {
            currentChatStatus = parseChatStatus(sakai.data.me.profile.chatstatus);
        }
        else {
            currentChatStatus = "online";
        }
        updateChatStatus();
    };

    ///////////////////////
    // Initial functions //
    ///////////////////////

    /**
     * Contains all the functions and methods that need to be
     * executed on the initial load of the page
     */
    var doInit = function(){
        var obj = {};
        var menulinks = [];

        for (var i in sakai.config.Navigation) {
            var temp = new Object();
            temp.url = sakai.config.Navigation[i].url;
            temp.label = sakai.api.i18n.General.getValueForKey(sakai.config.Navigation[i].label);
            if (i == 0) {
                temp.firstlink = true;
            }
            else {
                temp.firstlink = false;
            }
            menulinks.push(temp);
        }
        obj.links = menulinks;
        // Get navigation and render menu template
        $(".explore").html($.TemplateRenderer("navigation_template", obj));

        var person = sakai.data.me;

        $(exploreNavigationContainer).show();

        // Fill in the name of the user in the different fields
        if (sakai.api.User.getDisplayName(person.profile) !== "") {
            $(userIdLabel).text(sakai.api.User.getDisplayName(person.profile));
            $(hiLabel).text(sakai.api.User.getProfileBasicElementValue(person.profile, "firstName"));
        }

        // Show the profile picture on the dashboard page
        /** TODO : Remove the lines beneath if this functionality is inside changepic.js */
        if (person.profile.picture) {
            var picture = $.parseJSON(person.profile.picture);
            if (picture.name) {
                $(pictureHolder).attr("src", "/~" + sakai.data.me.user.userid + "/public/" + picture.name);
            }
        }

        // Highlight current nav item
        determineCurrentNav();

        // Set presence and bind things
        addBinding();
        getCountUnreadMessages();
        setPresence();

        // Get chat status
        getChatStatus();
    };

    if (sakai.data.me.user.anon) {
        // If a user is not logged in -> switch to anonymous mode
        switchToAnonymousMode();
    }
    else {
        doInit();
    }

};
sakai.api.Widgets.widgetLoader.informOnLoad("topnavigation");<commit_msg>SAKIII-652 put ID into variable for reuse<commit_after>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
/*global Config, $, jQuery, get_cookie, delete_cookie, set_cookie, window, alert */

var sakai = sakai || {};

/**
 * @name sakai.topnavigation
 *
 * @class topnavigation
 *
 * @description
 * Initialize the topnavigation widget
 *
 * @version 0.0.1
 * @param {String} tuid Unique id of the widget
 * @param {Boolean} showSettings Show the settings of the widget or not
 */
sakai.topnavigation = function(tuid, showSettings){

    /////////////////////////////
    // Configuration variables //
    /////////////////////////////

    var currentChatStatus = "";

    // Links and labels
    var hiLabel = "#hispan";
    var myprofileName = "#myprofile_name";
    var onlineButton = "#online_button";
    var pictureHolder = "#picture_holder";
    var showOnlineLink = "#show_online";
    var userIdLabel = "#userid";

    // User Link
    var userLink = "#user_link";
    var userLinkMenu = userLink + "_menu";
    var userLinkMenuLink = userLink + "_menu" + " a";

    // Navigation
    var nav = "#nav";
    var navContentMediaLink = "#nav_content_media_link";
    var navCoursesSitesLink = "#nav_courses_sites_link";
    var navCoursesSitesLinkClass = "#nav_courses_sites_link";
    var navPeopleLink = "#nav_people_link";
    var navPeopleLinkClass = "nav_people_link";
    var navMySakaiLink = "#nav_my_sakai_link";
    var navCalendarLink = "#nav_calendar_link";
    var navSelectedNavItemClass = "explore_nav_selected";

    // Messages
    var chatUnreadMessages = "#chat_unreadMessages";

    // Search
    var $general_search_form = $("#general_search_container form");
    var $general_search_input = $("#general_search_input");
    var $general_search_default_value = $("#general_search_default_value");
    var generalSearchSubmitButton = "#general_search_submit_button";
    var searchFocus = false;

    // Containers
    var exploreNavigationContainer = "#explore_nav_container";

    // CSS Classes
    var searchInputFocusClass = "search_input_focus";
    var chatAvailableStatusClass = "chat_available_status";
    var chatAvailableStatusClassOnline = chatAvailableStatusClass + "_online";
    var chatAvailableStatusClassBusy = chatAvailableStatusClass + "_busy";
    var chatAvailableStatusClassOffline = chatAvailableStatusClass + "_offline";

    var userLinkChatStatusClass = ".user_link_chat_status";

    ///////////////////////
    // Utility functions //
    ///////////////////////

    /**
     * Get the number of messages that are unread and show it.
     */
    var getCountUnreadMessages = function(){
        //we have the number of unread messages as a part of the me-feed
        //so get it directly from me object.
        $(chatUnreadMessages).text(sakai.data.me.messages.unread);
    };

    /**
     * Update a certain element with a specific chatstatus
     * @param {Object} element Element that needs to be updated
     * @param {String} chatstatus The chatstatus that needs to be added
     */
    var updateChatStatusElement = function(element, chatstatus){
        element.removeClass(chatAvailableStatusClassOnline);
        element.removeClass(chatAvailableStatusClassBusy);
        element.removeClass(chatAvailableStatusClassOffline);
        element.addClass(chatAvailableStatusClass + "_" + chatstatus);
    };

    /**
     * Update the status on the page by firing an event that handles this
     */
    var updateChatStatus = function(){
        // Trigger the chat_status_change event to update other widgets
        $(window).trigger("chat_status_change", currentChatStatus);
    };

    /**
     * Set the chatstatus of the user
     * @param {String} chatstatus The chatstatus which should be
     * online/offline or busy
     */
    var sendChatStatus = function(chatstatus){
        currentChatStatus = chatstatus;

        var data = {
            "chatstatus": chatstatus,
            "_charset_": "utf-8"
        };

        $.ajax({
            url: sakai.data.me.profile["jcr:path"],
            type: "POST",
            data: data,
            success: function(data){
                updateChatStatus();
            },
            error: function(xhr, textStatus, thrownError){
                alert("An error occurend when sending the status to the server.");
            }
        });
    };

    /**
     * Show or hide the user link menu
     * @param {Boolean} hideOnly
     *  true: Hide the menu only
     *  false: Show or hide the menu depending if it's already visible
     */
    var showHideUserLinkMenu = function(hideOnly){
        if ($(userLinkMenu).is(":visible") || hideOnly) {
            $(userLinkMenu).hide();
        }
        else {
            $(userLinkMenu).css("left", Math.round($(userLink).offset().left) + "px");
            $(userLinkMenu).css("top", (Math.round($(userLink).offset().top) + $(userLink).height() + 2) + "px");
            $(userLinkMenu).css("width", ($(userLink).width() + 10) + "px");
            $(userLinkMenu).show();
        }
    };

    /**
     * Add binding to some elements
     */
    var addBinding = function(){
        $(userLink).bind("click", function(){
            showHideUserLinkMenu(false);
        });

        $(userLinkChatStatusClass).bind("click", function(ev){
            showHideUserLinkMenu(false);
            var clicked = ev.currentTarget.id.split("_")[ev.currentTarget.id.split("_").length - 1];
            sendChatStatus(clicked);
        });

        $(document).bind("click", function(e){
            var $clicked = $(e.target);

            // Check if one of the parents is the userLink
            if (!$clicked.parents().is(userLink)) {
                showHideUserLinkMenu(true);
            }
        });

        $(window).bind("chat_status_change", function(event, currentChatStatus){
            updateChatStatusElement($(userLink), currentChatStatus)
        });
    };

    ////////////////
    // NAVIGATION //
    ///////////////

    /**
     * Select the page in the top navigation where you are currently on.
     * This will apply a class to the selected navigation item based on the current URL
     * @returns void;
     */
    var determineCurrentNav = function(){
        var windowLocationPath = window.location.pathname.toLowerCase();

        // Remove all selected classes from nav elements
        $(nav + " " + navSelectedNavItemClass).removeClass(navSelectedNavItemClass);

        // My Sakai
        if ((windowLocationPath.indexOf(sakai.config.URL.MY_DASHBOARD_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_MY_DASHBOARD_URL) !== -1)) {
            $(navMySakaiLink).addClass(navSelectedNavItemClass);
            return;
        }

        // Content & Media
        if ((windowLocationPath.indexOf(sakai.config.URL.CONTENT_MEDIA_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_CONTENT_MEDIA_URL) !== -1)) {
            $(navContentMediaLink).addClass(navSelectedNavItemClass);
            return;
        }

        // People
        if ((windowLocationPath.indexOf(sakai.config.URL.PEOPLE_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_PEOPLE_URL) !== -1)) {
            $(navPeopleLink).addClass(navSelectedNavItemClass);
            return;
        }

        // Courses & Sites
        if ((windowLocationPath.indexOf(sakai.config.URL.COURSES_SITES_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_COURSES_SITES_URL) !== -1) || (windowLocationPath.indexOf("/sites/") !== -1)) {
            $(navCoursesSitesLink).addClass(navSelectedNavItemClass);
            return;
        }

        // Calendar
        if ((windowLocationPath.indexOf(sakai.config.URL.SEARCH_GENERAL_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.SEARCH_PEOPLE_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.SEARCH_SITES_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.SEARCH_CONTENT_URL) !== -1) || (windowLocationPath.indexOf(sakai.config.URL.PUBLIC_SEARCH_URL) !== -1)) {
            $(navCalendarLink).addClass(navSelectedNavItemClass);
            return;
        }

    };

    ////////////
    // SEARCH //
    ////////////

    /**
     * Execute the search for the value that is in the search input field
     */
    var doSearch = function(){
        var tosearch = $general_search_input.val();
        // Disable search button
        $(generalSearchSubmitButton).attr("disabled", true);
        if (tosearch) {
            // Redirecting back to the general search page. This expects the URL to be
            // in a format like this one: page.html#pageid|searchstring
            document.location = sakai.config.URL.SEARCH_GENERAL_URL + "#q=" + tosearch;
            // Only enable button if the location is the search page
            if (window.location.pathname.split("/")[2] === "search.html") {
                $(generalSearchSubmitButton).attr("disabled", false);
            }
        }
    };

    /**
     * If this is the first time the field gets focus, we'll make his text color black
     * and remove the default value
     */
    $general_search_input.bind("focus", function(ev){
        if (!searchFocus) {
            $general_search_input.val("").addClass(searchInputFocusClass);
            searchFocus = true;
        }
    });

    /**
     * If we leave the field without filling out anything, we'll reinsert the default
     * search inputbox value
     */
    $general_search_input.bind("blur", function(ev){

        if (!$general_search_input.val()) {
            $general_search_input.removeClass(searchInputFocusClass);
            $general_search_input.val($general_search_default_value.text());
            searchFocus = false;
        }
    });

    /**
     * Bind the submit event to the search form
     * This event is triggered when you hit enter in the input field and
     * when you click on the submit button
     */
    $general_search_form.bind("submit", function(){

        doSearch();
        return false;
    });

    /**
     * Switch navigation bar to anonymous mode
     * @returns void
     */
    var switchToAnonymousMode = function(){

        // Show Nav Container
        $(exploreNavigationContainer).show();

        // Hide things which are irrelvant for Anonymous user
        $(".personal .mail").hide();
        $(".personal .sign_out").hide();
        $("#user_link_container").hide();

        // Show anonymous elements
        $("#other_logins_button_container").show();
        $("#register_button_container").show();
        $("#login_button_container").show();

        // Set up public nav links
        $("#nav_my_sakai_link a").attr("href", sakai.config.URL.PUBLIC_MY_DASHBOARD_URL);
        $("#nav_content_media_link a").attr("href", sakai.config.URL.PUBLIC_CONTENT_MEDIA_URL_PAGE);
        $("#nav_people_link a").attr("href", sakai.config.URL.PUBLIC_PEOPLE_URL);
        $("#nav_courses_sites_link a").attr("href", sakai.config.URL.PUBLIC_COURSES_SITES_URL);
        $("#nav_search_link a").attr("href", sakai.config.URL.PUBLIC_SEARCH_URL_PAGE);

        // Make the login page redirect to the current page after login
        $(".log_in").attr("href", $(".log_in").attr("href") + "?url=" + escape(window.location.pathname + window.location.search + window.location.hash));

    };

    /**
     * Set the presence for the current user
     * We need to do this in order to let the server know the user is still available for chatting.
     */
    var setPresence = function(){

        var data = {
            "sakai:status": "online",
            "_charset_": "utf-8"
        };

        $.ajax({
            url: sakai.config.URL.PRESENCE_SERVICE,
            type: "POST",
            success: function(){
                setTimeout(setPresence, 120000);
            },
            data: data
        });
    };

    /**
     * Parse the chatstatus for a user
     * @param {String} chatStatus The chatstatus which should be
     * online, busy or offline
     */
    var parseChatStatus = function(chatStatus){
        // Check if the status is defined
        if (!chatStatus) {
            chatStatus = "online";
        }
        return chatStatus;
    };

    /**
     * Get the chat status for the current user
     */
    var getChatStatus = function(){
        if (sakai.data.me.profile) {
            currentChatStatus = parseChatStatus(sakai.data.me.profile.chatstatus);
        }
        else {
            currentChatStatus = "online";
        }
        updateChatStatus();
    };

    ///////////////////////
    // Initial functions //
    ///////////////////////

    /**
     * Contains all the functions and methods that need to be
     * executed on the initial load of the page
     */
    var doInit = function(){
        var obj = {};
        var menulinks = [];

        for (var i in sakai.config.Navigation) {
            var temp = new Object();
            temp.url = sakai.config.Navigation[i].url;
            temp.label = sakai.api.i18n.General.getValueForKey(sakai.config.Navigation[i].label);
            if (i == 0) {
                temp.firstlink = true;
            }
            else {
                temp.firstlink = false;
            }
            menulinks.push(temp);
        }
        obj.links = menulinks;
        // Get navigation and render menu template
        $(".explore").html($.TemplateRenderer("navigation_template", obj));

        var person = sakai.data.me;

        $(exploreNavigationContainer).show();

        // Fill in the name of the user in the different fields
        if (sakai.api.User.getDisplayName(person.profile) !== "") {
            $(userIdLabel).text(sakai.api.User.getDisplayName(person.profile));
            $(hiLabel).text(sakai.api.User.getProfileBasicElementValue(person.profile, "firstName"));
        }

        // Show the profile picture on the dashboard page
        /** TODO : Remove the lines beneath if this functionality is inside changepic.js */
        if (person.profile.picture) {
            var picture = $.parseJSON(person.profile.picture);
            if (picture.name) {
                $(pictureHolder).attr("src", "/~" + sakai.data.me.user.userid + "/public/" + picture.name);
            }
        }

        // Highlight current nav item
        determineCurrentNav();

        // Set presence and bind things
        addBinding();
        getCountUnreadMessages();
        setPresence();

        // Get chat status
        getChatStatus();
    };

    if (sakai.data.me.user.anon) {
        // If a user is not logged in -> switch to anonymous mode
        switchToAnonymousMode();
    }
    else {
        doInit();
    }

};
sakai.api.Widgets.widgetLoader.informOnLoad("topnavigation");<|endoftext|>


<commit_before>// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('../..')(server);
var port = process.env.PORT || 3000;

server.listen(port, function () {
  console.log('Server listening at port %d', port);
});

// Routing
app.use(express.static(__dirname + '/public'));

// Chatroom

// usernames which are currently connected to the chat
var usernames = {};
var numUsers = 0;

io.on('connection', function (socket) {
  var addedUser = false;

  // when the client emits 'new message', this listens and executes
  socket.on('new message', function (data) {
    // we tell the client to execute 'new message'
    socket.broadcast.emit('new message', {
      username: socket.username,
      message: data
    });
  });

  // when the client emits 'add user', this listens and executes
  socket.on('add user', function (username) {
    // we store the username in the socket session for this client
    socket.username = username;
    // add the client's username to the global list
    usernames[username] = username;
    ++numUsers;
    addedUser = true;
    socket.emit('login', {
      numUsers: numUsers
    });
    // echo globally (all clients) that a person has connected
    socket.broadcast.emit('user joined', {
      username: socket.username,
      numUsers: numUsers
    });
  });

  // when the client emits 'typing', we broadcast it to others
  socket.on('typing', function () {
    socket.broadcast.emit('typing', {
      username: socket.username
    });
  });

  // when the client emits 'stop typing', we broadcast it to others
  socket.on('stop typing', function () {
    socket.broadcast.emit('stop typing', {
      username: socket.username
    });
  });

  // when the user disconnects.. perform this
  socket.on('disconnect', function () {
    // remove the username from global usernames list
    if (addedUser) {
      delete usernames[socket.username];
      --numUsers;

      // echo globally that this client has left
      socket.broadcast.emit('user left', {
        username: socket.username,
        numUsers: numUsers
      });
    }
  });
});
<commit_msg>improve the chat demo<commit_after>// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('../..')(server);
var port = process.env.PORT || 3000;

server.listen(port, function () {
  console.log('Server listening at port %d', port);
});

// Routing
app.use(express.static(__dirname + '/public'));

// Chatroom

var numUsers = 0;

io.on('connection', function (socket) {
  var addedUser = false;

  // when the client emits 'new message', this listens and executes
  socket.on('new message', function (data) {
    // we tell the client to execute 'new message'
    socket.broadcast.emit('new message', {
      username: socket.username,
      message: data
    });
  });

  // when the client emits 'add user', this listens and executes
  socket.on('add user', function (username) {
    if (addedUser) return;

    // we store the username in the socket session for this client
    socket.username = username;
    ++numUsers;
    addedUser = true;
    socket.emit('login', {
      numUsers: numUsers
    });
    // echo globally (all clients) that a person has connected
    socket.broadcast.emit('user joined', {
      username: socket.username,
      numUsers: numUsers
    });
  });

  // when the client emits 'typing', we broadcast it to others
  socket.on('typing', function () {
    socket.broadcast.emit('typing', {
      username: socket.username
    });
  });

  // when the client emits 'stop typing', we broadcast it to others
  socket.on('stop typing', function () {
    socket.broadcast.emit('stop typing', {
      username: socket.username
    });
  });

  // when the user disconnects.. perform this
  socket.on('disconnect', function () {
    if (addedUser) {
      --numUsers;

      // echo globally that this client has left
      socket.broadcast.emit('user left', {
        username: socket.username,
        numUsers: numUsers
      });
    }
  });
});
<|endoftext|>


<commit_before>/**
 * @fileOverview reading_process.js manages the process of reading content
 * injected into host pages. The process is as follows:
 *
 * 1. The manifest.json file injects a content script, privly.js, into every
 *    websites the user visits, including iframes.
 * 2. This background script messages the privly.js content script the user's
 *    whitelist. This whitelist defines domains the user trust in addition to
 *    the default white list. see: readingProcess.updateContentScriptWhitelist()
 * 3. This background script checks the state of the modal button, and turns
 *    on the privly.js content script if the modal button is on. This
 *    background script also defines a click listener on the modal button,
 *    that will start or stop the content scripts depending on the new state
 *    of the button. see: readingProcess.tabChange()
 * 4. The content script, privly.js, discovers whitelisted links or
 *    clicks on a passive link, then it requests a source URL from the
 *    extension using the message interface. The source URL
 *    will be assigned to the URL returned by
 *    readingProcess.getApplicationInjectionUrlResponse(). If the
 *    application behind the URL is known, the application will be served
 *    from local storage and not the remote content server.
 *
 **/

/**
 * @namespace For functionality related to reading content injected into
 * a host page.
 */
var readingProcess = {

  /**
   * If the content script has been injected, this will ask it to run.
   *
   * @param {tab} tabId The integer identifier of the tab to activate with
   * the content script.
   *
   * @see readingProcess.deactivateContentInjectionScript
   */
  activateContentInjectionScript: function (tabId) {
    chrome.tabs.executeScript(tabId, {
        code: "if(typeof privly != 'undefined'){privly.start();}",
        allFrames: true
    });
  },

  /**
   * If the content script has been injected, this will ask it to not run.
   *
   * @param {tab} tabId The integer identifier of the tab who needs to stop
   * running the content script.
   *
   * @see readingProcess.activateContentInjectionScript
   */
  deactivateContentInjectionScript: function(tabId) {
    chrome.tabs.executeScript(tabId, {
        code: "if(typeof privly != 'undefined'){privly.stop();}",
        allFrames: true
    });
  },

  /**
   * Notifies content script of additions to injection whitelist. This list is
   * assigned from the options page.
   *
   * @param {integer} tabId The integer identifier of the tab who needs to be told
   * the updated whitelist.
   *
   */
  updateContentScriptWhitelist: function(tabId) {
    var user_whitelist_regexp = localStorage["user_whitelist_regexp"];
    if (!user_whitelist_regexp) {
      return;
    }
    chrome.tabs.executeScript(tabId, {
        code: "if(typeof privly != 'undefined'){privly.updateWhitelist('"+user_whitelist_regexp+"');}",
        allFrames: true
    });
  },

  /**
   * Callback assigns content script state according to the modal button.
   *
   * @param {tab} tab The tab that
   * needs to be sent the operation mode and user's whitelist.
   */
  tabChange: function(tab) {

    if (tab.status !== "complete" ||
        tab.title === "New Tab" ||          // Solves hard to replicate error
        (tab.url.indexOf("http") !== 0 &&
         tab.url.indexOf("file") !== 0)) {
           return;
    }

    readingProcess.updateContentScriptWhitelist(tab.id);
    if( modalButton.badgeText === "off" ) {
      readingProcess.deactivateContentInjectionScript(tab.id);
    } else {
      readingProcess.activateContentInjectionScript(tab.id);
    }

  },

  /**
   * Helper for updating multiple tabs simultaneously. Relies on
   * the readingProcess.tabChange function.
   *
   * @param {[tab,...]} tabs The array of tabs that
   * needs to be sent the operation mode and user's whitelist.
   */
  tabsChange: function(tabs) {

    // Facilitate modifying an array of tabs by recursively
    // calling this function on every tab.
    if ( typeof tabs.length !== "undefined" ) {
      for( var i = 0; i < tabs.length; i++)
        readingProcess.tabChange(tabs[i]);
      return;
    }
  },

  /**
   * Gives the URL to inject an iframe from local storage if it is a known
   * application.
   *
   * @param {object} request The json request object sent by the content scrpt.
   * @param {object} sender The sender of the message.
   * @param {function} sendResponse The callback function of the message from
   * the content script.
   *
   */
  getApplicationInjectionUrlResponse: function(request, sender, sendResponse) {
    var url = request.privlyOriginalURL;

    if( url.indexOf("privlyApp=Message") > 0 ) {
      sendResponse({
        privlyApplicationURL:
          chrome.extension.getURL(
            "privly-applications/Message/show.html?privlyOriginalURL=" +
            encodeURIComponent(url))});
    } else if( url.indexOf("privlyApp=PlainPost" ) > 0) {
      sendResponse({
        privlyApplicationURL:
          chrome.extension.getURL("privly-applications/PlainPost/show.html?privlyOriginalURL=" +
          encodeURIComponent(url))});
    } else {
      console.warn("Injectable App not specified, defaulting to sanitized " +
                   "PlainPost: "+ request.privlyOriginalURL);
      sendResponse({
        privlyApplicationURL:
          chrome.extension.getURL("privly-applications/PlainPost/show.html?privlyOriginalURL=" +
          encodeURIComponent(url))});
    }
  },

  /**
   * Monitor tabs and messages to facilitate reading injected content.
   */
  addListeners: function() {

    // Message listeners are currently distinguised by whether they contain
    // the appropriate JSON in their request.
    chrome.extension.onMessage.addListener(
      function(request, sender, sendResponse) {
        if (request.privlyOriginalURL !== undefined) {
          return readingProcess.getApplicationInjectionUrlResponse(request, sender, sendResponse);
        }
      });

    // When the active tab changes we must update the tab's content script for
    // the current operation mode of the modal button.
    chrome.tabs.onActivated.addListener(function(activeInfo) {
      chrome.tabs.get(activeInfo.tabId, readingProcess.tabChange);
    });

    // Respond to every request to start the content script.
    chrome.runtime.onMessage.addListener(
      function(request, sender, sendResponse) {
        if (request.ask === "shouldStartPrivly?") {
          if(modalButton.badgeText !== "off") {
            sendResponse({tell: "yes"});
          }
        }
    });
  }
}

readingProcess.addListeners();
<commit_msg>Add fallback for injection of deprecated ZeroBin<commit_after>/**
 * @fileOverview reading_process.js manages the process of reading content
 * injected into host pages. The process is as follows:
 *
 * 1. The manifest.json file injects a content script, privly.js, into every
 *    websites the user visits, including iframes.
 * 2. This background script messages the privly.js content script the user's
 *    whitelist. This whitelist defines domains the user trust in addition to
 *    the default white list. see: readingProcess.updateContentScriptWhitelist()
 * 3. This background script checks the state of the modal button, and turns
 *    on the privly.js content script if the modal button is on. This
 *    background script also defines a click listener on the modal button,
 *    that will start or stop the content scripts depending on the new state
 *    of the button. see: readingProcess.tabChange()
 * 4. The content script, privly.js, discovers whitelisted links or
 *    clicks on a passive link, then it requests a source URL from the
 *    extension using the message interface. The source URL
 *    will be assigned to the URL returned by
 *    readingProcess.getApplicationInjectionUrlResponse(). If the
 *    application behind the URL is known, the application will be served
 *    from local storage and not the remote content server.
 *
 **/

/**
 * @namespace For functionality related to reading content injected into
 * a host page.
 */
var readingProcess = {

  /**
   * If the content script has been injected, this will ask it to run.
   *
   * @param {tab} tabId The integer identifier of the tab to activate with
   * the content script.
   *
   * @see readingProcess.deactivateContentInjectionScript
   */
  activateContentInjectionScript: function (tabId) {
    chrome.tabs.executeScript(tabId, {
        code: "if(typeof privly != 'undefined'){privly.start();}",
        allFrames: true
    });
  },

  /**
   * If the content script has been injected, this will ask it to not run.
   *
   * @param {tab} tabId The integer identifier of the tab who needs to stop
   * running the content script.
   *
   * @see readingProcess.activateContentInjectionScript
   */
  deactivateContentInjectionScript: function(tabId) {
    chrome.tabs.executeScript(tabId, {
        code: "if(typeof privly != 'undefined'){privly.stop();}",
        allFrames: true
    });
  },

  /**
   * Notifies content script of additions to injection whitelist. This list is
   * assigned from the options page.
   *
   * @param {integer} tabId The integer identifier of the tab who needs to be told
   * the updated whitelist.
   *
   */
  updateContentScriptWhitelist: function(tabId) {
    var user_whitelist_regexp = localStorage["user_whitelist_regexp"];
    if (!user_whitelist_regexp) {
      return;
    }
    chrome.tabs.executeScript(tabId, {
        code: "if(typeof privly != 'undefined'){privly.updateWhitelist('"+user_whitelist_regexp+"');}",
        allFrames: true
    });
  },

  /**
   * Callback assigns content script state according to the modal button.
   *
   * @param {tab} tab The tab that
   * needs to be sent the operation mode and user's whitelist.
   */
  tabChange: function(tab) {

    if (tab.status !== "complete" ||
        tab.title === "New Tab" ||          // Solves hard to replicate error
        (tab.url.indexOf("http") !== 0 &&
         tab.url.indexOf("file") !== 0)) {
           return;
    }

    readingProcess.updateContentScriptWhitelist(tab.id);
    if( modalButton.badgeText === "off" ) {
      readingProcess.deactivateContentInjectionScript(tab.id);
    } else {
      readingProcess.activateContentInjectionScript(tab.id);
    }

  },

  /**
   * Helper for updating multiple tabs simultaneously. Relies on
   * the readingProcess.tabChange function.
   *
   * @param {[tab,...]} tabs The array of tabs that
   * needs to be sent the operation mode and user's whitelist.
   */
  tabsChange: function(tabs) {

    // Facilitate modifying an array of tabs by recursively
    // calling this function on every tab.
    if ( typeof tabs.length !== "undefined" ) {
      for( var i = 0; i < tabs.length; i++)
        readingProcess.tabChange(tabs[i]);
      return;
    }
  },

  /**
   * Gives the URL to inject an iframe from local storage if it is a known
   * application.
   *
   * @param {object} request The json request object sent by the content scrpt.
   * @param {object} sender The sender of the message.
   * @param {function} sendResponse The callback function of the message from
   * the content script.
   *
   */
  getApplicationInjectionUrlResponse: function(request, sender, sendResponse) {
    var url = request.privlyOriginalURL;
    var response = "";

    if( url.indexOf("privlyApp=Message") > 0 ) {
      response = chrome.extension.getURL(
                  "privly-applications/Message/show.html?privlyOriginalURL=");
    } else if( url.indexOf("privlyApp=ZeroBin") > 0) {
      // Deprecated
      response = chrome.extension.getURL(
                  "privly-applications/Message/show.html?privlyOriginalURL=");
    } else if( url.indexOf("privlyApp=PlainPost") > 0) {
      response = chrome.extension.getURL(
                  "privly-applications/PlainPost/show.html?privlyOriginalURL=");
    } else {
      console.warn("Injectable App not specified, defaulting to sanitized " +
                   "PlainPost: "+ request.privlyOriginalURL);
      response = chrome.extension.getURL(
                  "privly-applications/PlainPost/show.html?privlyOriginalURL=");
    }
    response += encodedURIComponent(url);
    sendResponse( {privlyApplicationURL: response} );
  },

  /**
   * Monitor tabs and messages to facilitate reading injected content.
   */
  addListeners: function() {

    // Message listeners are currently distinguised by whether they contain
    // the appropriate JSON in their request.
    chrome.extension.onMessage.addListener(
      function(request, sender, sendResponse) {
        if (request.privlyOriginalURL !== undefined) {
          return readingProcess.getApplicationInjectionUrlResponse(request, sender, sendResponse);
        }
      });

    // When the active tab changes we must update the tab's content script for
    // the current operation mode of the modal button.
    chrome.tabs.onActivated.addListener(function(activeInfo) {
      chrome.tabs.get(activeInfo.tabId, readingProcess.tabChange);
    });

    // Respond to every request to start the content script.
    chrome.runtime.onMessage.addListener(
      function(request, sender, sendResponse) {
        if (request.ask === "shouldStartPrivly?") {
          if(modalButton.badgeText !== "off") {
            sendResponse({tell: "yes"});
          }
        }
    });
  }
}

readingProcess.addListeners();
<|endoftext|>


<commit_before>module.exports = {
  name: "pluginB",
  description: "Plugin B",
  services: {
    "B.a": {
      module: './a.js',
      async: true
      require: ["timeout", "A.b"]
    }
  },
  exports: ['B.a']
};<commit_msg>add missing comma<commit_after>module.exports = {
  name: "pluginB",
  description: "Plugin B",
  services: {
    "B.a": {
      module: './a.js',
      async: true,
      require: ["timeout", "A.b"]
    }
  },
  exports: ['B.a']
};<|endoftext|>


<commit_before>function PhotoViewModel(data) {
  var self = this;

  self.stdAspectRatios = [[4, 3], [3, 2], [16, 9], [5, 3], [5, 4], [1, 1]];

  self.data = data;

  self.aspectRatio = function (width, height) {
    var gcd = self.gcd(width, height);
    return [width / gcd, height / gcd];
  };

  self.aspectRatioText = function () {
    var aspectRatio = self.aspectRatio(self.data.width, self.data.height);
    var exactMatch = _.find(self.stdAspectRatios, function (ar) {
      if (self.isPortrait()) {
        ar[1] = [ar[0], ar[0] = ar[1]][0]; // swap hack
      }

      return ar[0] == aspectRatio[0] && ar[1] == aspectRatio[1];
    });

    if (exactMatch) { return aspectRatio.join(':'); }

    var closestMatch = _.sortBy(self.stdAspectRatios, function (ar) {
      return Math.abs((ar[0] / ar[1]) - (aspectRatio[0] / aspectRatio[1]));
    })[0];

    return '~' + closestMatch.join(':');
  };

  self.fileName = function () {
    return self.data.path.split('/').reverse()[0];
  };

  self.fileSize =  function () {
    return self.formatSize(self.data.size);
  };

  self.focalLength = function () {
    var fl = self.data.exif.FocalLength;
    var fl35 = self.data.exif.FocalLengthIn35mmFormat;

    if (!fl) {
      return;
    }

    fl = fl.replace('.0', '');

    if (!fl35) {
      return fl;
    }

    fl35 = fl35.replace('.0', '');

    if (fl === fl35) {
      return fl;
    }

    return fl + ' (' + fl35 + ')';
  };

  self.formatSize = function (bytes) {
    var n = bytes;

    if (n < 1024) {
      return _.pluralize(n, 'byte');
    }

    n /= 1024; // kB

    if (n < 1024) {
      return self.round(n, 2) + ' kB';
    }

    n /= 1024; // MB

    return self.round(n, 2) + ' MB';
  };

  self.gcd = function (a, b) {
    return b ? self.gcd(b, a % b) : Math.abs(a);
  };

  self.isPortrait = function () {
    return self.data.width < self.data.height;
  };

  self.round = function (n, scale) {
    return Math.round(n * Math.pow(10, scale)) / Math.pow(10, scale);
  }

  self.shortName = function () {
    return _.trunc(self.fileName(), 20);
  };

  self.takenAtText = function () {
    return moment(self.data.takenAt).fromNow();
  };

  self.takenAtTitle = function () {
    return moment(self.data.takenAt).format('ddd, D MMM YYYY HH:mm:ss z');
  };
}

function SetViewModel(data) {
  var self = this;

  self.data = data;

  self.photosCountTitle = function () {
    return _.pluralize(self.data.photosCount, 'photo');
  };

  self.shortName = function () {
    return _.trunc(self.data.name, 20);
  };
}

function ThymeViewModel() {
  var self = this;

  self.photo = _.tap(ko.observable(), function (observable) {
    observable.subscribe(function (photo) {
      if (photo) {
        location.hash = photo.data.setId + '/' + photo.data.id;
      }
    });
  });

  self.photos = ko.observableArray([]);

  self.set = _.tap(ko.observable(), function (observable) {
    observable.subscribe(function (set) {
      location.hash = set.data.id;
    });
  });

  self.sets = ko.observableArray([]);

  Sammy(function () {
    this.get('#:setId/:photoId', function () {
      if (self.photo()) { return; } // clicked photo

      var data = {
        id: this.params.photoId,
        set_id: this.params.setId
      };

      $.getJSON('/photo', data, function (photoData) {
        self.photo(new PhotoViewModel(photoData));
      });
    });

    this.get('#:setId', function () {
      self.photo(null);

      if (self.photos().length > 0) { // back pressed
        return;
      }

      $.getJSON('/photos', { set_id: this.params.setId }, function (photosData) {
        _.forEach(photosData, function (photoData) {
          self.photos.push(new PhotoViewModel(photoData));
        });
      });
    });

    this.get('', function () {
      self.photos([]);

      if (self.sets().length > 0) { // back pressed
        return;
      }

      $.getJSON('/sets', function (setsData) {
        _.forEach(setsData, function (setData) {
          self.sets.push(new SetViewModel(setData));
        });
      });
    });
  }).run();
};

// lodash extensions
_.mixin({
  pluralize: function (n, singular, plural) {
    plural = plural || singular + 's';
    return n + ' ' + (n === 1 ? singular : plural);
  }
});

ko.applyBindings(new ThymeViewModel());
<commit_msg>Explain "~" is an approximation in aspect ratio HTML<commit_after>function PhotoViewModel(data) {
  var self = this;

  self.stdAspectRatios = [[4, 3], [3, 2], [16, 9], [5, 3], [5, 4], [1, 1]];

  self.data = data;

  self.aspectRatio = function (width, height) {
    var gcd = self.gcd(width, height);
    return [width / gcd, height / gcd];
  };

  self.aspectRatioText = function () {
    var aspectRatio = self.aspectRatio(self.data.width, self.data.height);
    var exactMatch = _.find(self.stdAspectRatios, function (ar) {
      if (self.isPortrait()) {
        ar[1] = [ar[0], ar[0] = ar[1]][0]; // swap hack
      }

      return ar[0] == aspectRatio[0] && ar[1] == aspectRatio[1];
    });

    if (exactMatch) { return aspectRatio.join(':'); }

    var closestMatch = _.sortBy(self.stdAspectRatios, function (ar) {
      return Math.abs((ar[0] / ar[1]) - (aspectRatio[0] / aspectRatio[1]));
    })[0];

    return '~' + closestMatch.join(':');
  };

  self.fileName = function () {
    return self.data.path.split('/').reverse()[0];
  };

  self.fileSize =  function () {
    return self.formatSize(self.data.size);
  };

  self.focalLength = function () {
    var fl = self.data.exif.FocalLength;
    var fl35 = self.data.exif.FocalLengthIn35mmFormat;

    if (!fl) {
      return;
    }

    fl = fl.replace('.0', '');

    if (!fl35) {
      return fl;
    }

    fl35 = fl35.replace('.0', '');

    if (fl === fl35) {
      return fl;
    }

    return fl + ' (' + fl35 + ')';
  };

  self.formatSize = function (bytes) {
    var n = bytes;

    if (n < 1024) {
      return _.pluralize(n, 'byte');
    }

    n /= 1024; // kB

    if (n < 1024) {
      return self.round(n, 2) + ' kB';
    }

    n /= 1024; // MB

    return self.round(n, 2) + ' MB';
  };

  self.gcd = function (a, b) {
    return b ? self.gcd(b, a % b) : Math.abs(a);
  };

  self.isPortrait = function () {
    return self.data.width < self.data.height;
  };

  self.round = function (n, scale) {
    return Math.round(n * Math.pow(10, scale)) / Math.pow(10, scale);
  }

  self.shortName = function () {
    return _.trunc(self.fileName(), 20);
  };

  self.takenAtText = function () {
    return moment(self.data.takenAt).fromNow();
  };

  self.takenAtTitle = function () {
    return moment(self.data.takenAt).format('ddd, D MMM YYYY HH:mm:ss z');
  };
}

function SetViewModel(data) {
  var self = this;

  self.data = data;

  self.photosCountTitle = function () {
    return _.pluralize(self.data.photosCount, 'photo');
  };

  self.shortName = function () {
    return _.trunc(self.data.name, 20);
  };
}

function ThymeViewModel() {
  var self = this;

  self.photo = _.tap(ko.observable(), function (observable) {
    observable.subscribe(function (photo) {
      if (photo) {
        location.hash = photo.data.setId + '/' + photo.data.id;
      }
    });
  });

  self.photos = ko.observableArray([]);

  self.set = _.tap(ko.observable(), function (observable) {
    observable.subscribe(function (set) {
      location.hash = set.data.id;
    });
  });

  self.sets = ko.observableArray([]);

  Sammy(function () {
    this.get('#:setId/:photoId', function () {
      if (self.photo()) { return; } // clicked photo

      var data = {
        id: this.params.photoId,
        set_id: this.params.setId
      };

      $.getJSON('/photo', data, function (photoData) {
        self.photo(new PhotoViewModel(photoData));
      });
    });

    this.get('#:setId', function () {
      self.photo(null);

      if (self.photos().length > 0) { // back pressed
        return;
      }

      $.getJSON('/photos', { set_id: this.params.setId }, function (photosData) {
        _.forEach(photosData, function (photoData) {
          self.photos.push(new PhotoViewModel(photoData));
        });
      });
    });

    this.get('', function () {
      self.photos([]);

      if (self.sets().length > 0) { // back pressed
        return;
      }

      $.getJSON('/sets', function (setsData) {
        _.forEach(setsData, function (setData) {
          self.sets.push(new SetViewModel(setData));
        });
      });
    });
  }).run();
};

// lodash extensions
_.mixin({
  pluralize: function (n, singular, plural) {
    plural = plural || singular + 's';
    return n + ' ' + (n === 1 ? singular : plural);
  }
});

ko.applyBindings(new ThymeViewModel());
<|endoftext|>


<commit_before>import ExtendableError from 'es6-error';

import { captureException } from 'common/errorLogger';

import makeWclApiUrl from './makeWclApiUrl';

export class ApiDownError extends ExtendableError {}
export class LogNotFoundError extends ExtendableError {}
export class CharacterNotFoundError extends ExtendableError {}
export class JsonParseError extends ExtendableError {
  originalError = null;
  raw = null;
  constructor(originalError, raw) {
    super();
    this.originalError = originalError;
    this.raw = raw;
  }
}
export class WclApiError extends ExtendableError {}
export class UnknownApiError extends ExtendableError {}
export class CorruptResponseError extends ExtendableError {}

const HTTP_CODES = {
  OK: 200,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  CLOUDFLARE: {
    UNKNOWN_ERROR: 520,
    WEB_SERVER_IS_DOWN: 521,
    CONNECTION_TIMED_OUT: 522,
    ORIGIN_IS_UNREACHABLE: 523,
    A_TIMEOUT_OCCURED: 524,
  },
};
const WCL_API_ERROR_TEXT = 'Warcraft Logs API error';

async function rawFetchWcl(endpoint, queryParams) {
  const url = makeWclApiUrl(endpoint, queryParams);
  const response = await fetch(url);

  if (Object.values(HTTP_CODES.CLOUDFLARE).includes(response.status)) {
    throw new ApiDownError('The API is currently down. This is usually for maintenance which should only take about 10 seconds. Please try again in a moment.');
  }
  // Manually parse the response JSON so we keep the original data in memory so we can pass it to Sentry if something is wrong.
  let text = await response.text();
  let json = null;
  try {
    // remove crap from string before parsing JSON
    // hotfixes the german logs that have control-characters in their names
    text = text.replace(/[^\x20-\x7E]/g, '');
    json = JSON.parse(text);
  } catch (error) {
    captureException(error, {
      extra: {
        text,
      },
    });
    throw new JsonParseError();
  }

  if ([HTTP_CODES.BAD_REQUEST, HTTP_CODES.UNAUTHORIZED].includes(response.status)) {
    const message = json.message;
    if (message === 'This report does not exist or is private.') {
      throw new LogNotFoundError();
    }
    if (message === 'Invalid character name/server/region specified.') {
      throw new CharacterNotFoundError();
    }
    throw new Error(message || json.error);
  }
  if (!response.ok) {
    if (json.error === WCL_API_ERROR_TEXT) {
      throw new WclApiError(`${response.status}: ${json.message}`);
    } else {
      throw new UnknownApiError(`${response.status}: ${json.message}`);
    }
  }
  return json;
}

const defaultOptions = {
  timeout: 10000,
};
export default function fetchWclWithTimeout(endpoint, queryParams, options) {
  options = !options ? defaultOptions : { ...defaultOptions, ...options };

  return new Promise((resolve, reject) => {
    let timedOut = false;
    const timeoutTimer = setTimeout(() => {
      timedOut = true;
      reject(new Error('Request timed out. This usually happens because the Warcraft Logs API did not respond in a timely fashion. Try again.'));
    }, options.timeout);

    rawFetchWcl(endpoint, queryParams)
      .then(results => {
        clearTimeout(timeoutTimer);
        if (timedOut) {
          return;
        }
        resolve(results);
      })
      .catch(err => {
        clearTimeout(timeoutTimer);
        if (timedOut) {
          return;
        }
        reject(err);
      });
  });
}
<commit_msg>only run regex on 'unexpected character'-error<commit_after>import ExtendableError from 'es6-error';

import { captureException } from 'common/errorLogger';

import makeWclApiUrl from './makeWclApiUrl';

export class ApiDownError extends ExtendableError {}
export class LogNotFoundError extends ExtendableError {}
export class CharacterNotFoundError extends ExtendableError {}
export class JsonParseError extends ExtendableError {
  originalError = null;
  raw = null;
  constructor(originalError, raw) {
    super();
    this.originalError = originalError;
    this.raw = raw;
  }
}
export class WclApiError extends ExtendableError {}
export class UnknownApiError extends ExtendableError {}
export class CorruptResponseError extends ExtendableError {}

const HTTP_CODES = {
  OK: 200,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  CLOUDFLARE: {
    UNKNOWN_ERROR: 520,
    WEB_SERVER_IS_DOWN: 521,
    CONNECTION_TIMED_OUT: 522,
    ORIGIN_IS_UNREACHABLE: 523,
    A_TIMEOUT_OCCURED: 524,
  },
};
const WCL_API_ERROR_TEXT = 'Warcraft Logs API error';

async function rawFetchWcl(endpoint, queryParams) {
  const url = makeWclApiUrl(endpoint, queryParams);
  const response = await fetch(url);

  if (Object.values(HTTP_CODES.CLOUDFLARE).includes(response.status)) {
    throw new ApiDownError('The API is currently down. This is usually for maintenance which should only take about 10 seconds. Please try again in a moment.');
  }
  // Manually parse the response JSON so we keep the original data in memory so we can pass it to Sentry if something is wrong.
  let text = await response.text();
  let json = null;
  try {
    json = JSON.parse(text);
  } catch (error) {
    // trys to replace non-ascii chars on "unexpected character"-errors
    // hotfixes the german logs that have control-characters in their names
    if (error instanceof SyntaxError) {
      try {
        text = text.replace(/[^\x20-\x7E]/g, '');
        json = JSON.parse(text);
      } catch (asciiFixError) {
        captureException(asciiFixError, {
          extra: {
            text,
          },
        });
        throw new JsonParseError();
      }
    } else {
      captureException(error, {
        extra: {
          text,
        },
      });
      throw new JsonParseError();
    }
  }

  if ([HTTP_CODES.BAD_REQUEST, HTTP_CODES.UNAUTHORIZED].includes(response.status)) {
    const message = json.message;
    if (message === 'This report does not exist or is private.') {
      throw new LogNotFoundError();
    }
    if (message === 'Invalid character name/server/region specified.') {
      throw new CharacterNotFoundError();
    }
    throw new Error(message || json.error);
  }
  if (!response.ok) {
    if (json.error === WCL_API_ERROR_TEXT) {
      throw new WclApiError(`${response.status}: ${json.message}`);
    } else {
      throw new UnknownApiError(`${response.status}: ${json.message}`);
    }
  }
  return json;
}

const defaultOptions = {
  timeout: 10000,
};
export default function fetchWclWithTimeout(endpoint, queryParams, options) {
  options = !options ? defaultOptions : { ...defaultOptions, ...options };

  return new Promise((resolve, reject) => {
    let timedOut = false;
    const timeoutTimer = setTimeout(() => {
      timedOut = true;
      reject(new Error('Request timed out. This usually happens because the Warcraft Logs API did not respond in a timely fashion. Try again.'));
    }, options.timeout);

    rawFetchWcl(endpoint, queryParams)
      .then(results => {
        clearTimeout(timeoutTimer);
        if (timedOut) {
          return;
        }
        resolve(results);
      })
      .catch(err => {
        clearTimeout(timeoutTimer);
        if (timedOut) {
          return;
        }
        reject(err);
      });
  });
}
<|endoftext|>


<commit_before>// ==UserScript==
// @author      Robert Slifka (github @rslifka)
// @connect     docs.google.com
// @connect     rslifka.github.io
// @copyright   2017-2018, Robert Slifka (https://github.com/rslifka/fate_of_all_fools)
// @description Enhancements to the Destiny Item Manager
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getValue
// @grant       GM_log
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @homepage    https://github.com/rslifka/fate_of_all_fools
// @license     MIT; https://raw.githubusercontent.com/rslifka/fate_of_all_fools/master/LICENSE.txt
// @match       https://*.destinyitemmanager.com/*
// @name        Fate of All Fools
// @require     https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.js
// @resource    fateOfAllFoolsCSS https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.css
// @run-at      document-start
// @supportURL  https://github.com/rslifka/fate_of_all_fools/issues
// @version     330
// ==/UserScript==
<commit_msg>AUTO-CREATED Release Artifact v331 c/o Travis<commit_after>// ==UserScript==
// @author      Robert Slifka (github @rslifka)
// @connect     docs.google.com
// @connect     rslifka.github.io
// @copyright   2017-2018, Robert Slifka (https://github.com/rslifka/fate_of_all_fools)
// @description Enhancements to the Destiny Item Manager
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getValue
// @grant       GM_log
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @homepage    https://github.com/rslifka/fate_of_all_fools
// @license     MIT; https://raw.githubusercontent.com/rslifka/fate_of_all_fools/master/LICENSE.txt
// @match       https://*.destinyitemmanager.com/*
// @name        Fate of All Fools
// @require     https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.js
// @resource    fateOfAllFoolsCSS https://rslifka.github.io/fate_of_all_fools/fateOfAllFools.css
// @run-at      document-start
// @supportURL  https://github.com/rslifka/fate_of_all_fools/issues
// @version     331
// ==/UserScript==
<|endoftext|>


<commit_before>const fs = require('fs');

const Holmes = require('../js/holmes.js');

/**
 * Set the html stub
 */
const stub = fs.readFileSync('./__tests__/stub.html').toString();

function setStub() {
  document.body.innerHTML = stub;
}

/**
 * Enter a string into this Holmes input
 * @param  {string} text text to input
 * @return {Promise}     resolves when the input has been doen
 */
function input(text) {
  const holmesInput = document.getElementById('search');
  return new Promise((resolve, reject) => {
    if (holmesInput instanceof HTMLInputElement) {
      holmesInput.value = text;
    } else {
      holmesInput.textContent = text;
    }
    holmesInput.dispatchEvent(new Event('input'));
    resolve('input dispatched.');
  });
}

describe('Instance-less usage', () => {
  test('throws without options', () => {
    setStub();

    function init() {
      Holmes();
    }

    expect(init).toThrowError('The options need to be given inside an object like this:\nholmes({\n\tfind:".result"\n});\nsee also https://haroen.me/holmes/doc/module-holmes.html');
  });

  test('throws without .find', () => {
    setStub();

    function init() {
      Holmes({});
    }

    expect(init).toThrowError('A find argument is needed. That should be a querySelectorAll for each of the items you want to match individually. You should have something like: \nholmes({\n\tfind:".result"\n});\nsee also https://haroen.me/holmes/doc/module-holmes.html');
  });

  test('doesn\'t throw with .find given', () => {
    setStub();
    Holmes({
      find: '.result'
    });
  });

  // test('throws when .input you can\'t type in is given', () => {
  //   setStub();
  //   function init() {
  //     Holmes({
  //       find: '.result',
  //       input: '.result'
  //     });
  //
  //     input('bla');
  //   }
  //
  //   expect(init).toThrowError('The Holmes input was no <input> or contenteditable.');
  // });

  // test('works with contenteditable', () => {
  //   setStub();
  //   Holmes({
  //     find: '.result',
  //     input: '#contenteditable',
  //     class: {
  //       visible: 'visible'
  //     }
  //   });
  //
  //   input('special');
  //
  //   const special = document.getElementById('contains-special');
  //   const notSpecial = document.querySelectorAll('.result:not(#contains-special)');
  //   const notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
  //
  //   expect(special.classList.contains('visible')).toEqual(true);
  //   expect(notSpecial).toEqual(notSpecialHidden);
  // });
});

describe('options', () => {
  test('.visible on initialisation', () => {
    setStub();
    const find = '.result';
    Holmes({
      find,
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    const all = document.querySelectorAll(find);
    const visible = document.querySelectorAll(find + '.visible');

    expect(all).toEqual(visible);
  });

  describe('.minCharacters', () => {
    test('less than minimum amount doesn\'t make anything happen', () => {
      setStub();
      const find = '.result';
      Holmes({
        find,
        instant: true,
        minCharacters: 5,
        class: {
          visible: 'visible'
        }
      });
      //some short input that's surely not in the data
      input('qsdf');

      const all = document.querySelectorAll(find);
      const visible = document.querySelectorAll(find + '.visible');

      expect(all).toEqual(visible);
    });

    test('over minimum does make things happen', () => {
      setStub();
      const find = '.result';
      Holmes({
        find,
        instant: true,
        minCharacters: 5,
        class: {
          visible: 'visible'
        }
      });

      //some short input that's surely not in the data
      input('qsdfg');

      const all = document.querySelectorAll(find);
      const hidden = document.querySelectorAll(find + '.hidden');

      expect(all).toEqual(hidden);
    });
  });

  // describe('.mark', () => {
  //   test('', () => {
  //     setStub();
  //   });
  // });

  describe('callbacks', () => {
    // test('onHidden', () => {
    //   setStub();
    // });
    // test('onVisible', () => {
    //   setStub();
    // });
    // test('onEmpty', () => {
    //   setStub();
    // });
    // test('onFound', () => {
    //   setStub();
    // });
    // test('onInput', () => {
    //   setStub();
    // });
  });

  describe('.placeholder', () => {
    setStub();
    test('gets added', () => {
      Holmes({
        find: '.result',
        instant: true,
        placeholder: 'test'
      });

      const placeholder = document.getElementById('holmes-placeholder');

      expect(placeholder.innerHTML).toEqual('test');
    });

    test('can\'t be added when there\'s no parent of the elements', () => {
      function start() {
        Holmes({
          find: 'html',
          instant: true,
          placeholder: 'impossible'
        });
      }
      expect(start).toThrowError('The Holmes placeholder could\'t be put; the elements had no parent.');
    });

    test('initially hidden', () => {
      setStub();
      Holmes({
        find: '.result',
        instant: true,
        placeholder: 'test'
      });

      const placeholder = document.getElementById('holmes-placeholder');

      expect(placeholder.classList.contains('hidden')).toBe(true);
    });

    test('not hidden when no results', () => {
      setStub();
      Holmes({
        find: '.result',
        instant: true,
        placeholder: 'test'
      });

      const placeholder = document.getElementById('holmes-placeholder');

      return input('definitely not in the list').then(() => {
        expect(placeholder.classList.contains('hidden')).toBe(false);
      });
    });
  });
})

describe('Usage with instance', () => {
  test('Initialisation works', () => {
    setStub();
    const _h = new Holmes({
      find: '.result'
    });

    expect(_h).toBeDefined();
  });

  test('.clear() empties the input and shows everything', () => {
    setStub();
    const _h = new Holmes({
      find: '.result',
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    input('something');

    _h.clear();

    expect(document.getElementById('search').value).toBe('');

    const all = document.querySelectorAll('.result');
    const visible = document.querySelectorAll('.result' + '.visible');

    expect(all).toEqual(visible);

  });

  test('.stop() stops reacting to input', () => {
    setStub();
    const _h = new Holmes({
      find: '.result',
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    _h.stop();

    input('something');

    const all = document.querySelectorAll('.result');
    const visible = document.querySelectorAll('.result' + '.visible');

    expect(all).toEqual(visible);
  });

  test('.start() after .stop() resumes normal activity', () => {
    setStub();
    const _h = new Holmes({
      find: '.result',
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    const special = document.getElementById('contains-special');
    const notSpecial = document.querySelectorAll('.result:not(#contains-special)');
    let notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
    let notSpecialVisible = document.querySelectorAll('.result:not(#contains-special)' + '.visible');

    _h.stop();

    input('special');

    expect(special.classList.contains('visible')).toEqual(true);
    notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
    notSpecialVisible = document.querySelectorAll('.result:not(#contains-special)' + '.visible');
    expect(notSpecial).toEqual(notSpecialVisible);

    _h.start();

    input('special');

    expect(special.classList.contains('visible')).toEqual(true);
    notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
    notSpecialVisible = document.querySelectorAll('.result:not(#contains-special)' + '.visible');
    expect(notSpecial).toEqual(notSpecialHidden);

  });

  describe('.count()', () => {

    test('before input', () => {
      setStub();
      const _h = new Holmes({
        find: '.result',
        instant: true
      });

      expect(_h.count()).toEqual({
        all: 50,
        hidden: 0,
        visible: 50
      });
    });

    test('updates on input', () => {
      setStub();
      const _h = new Holmes({
        find: '.result',
        instant: true
      });

      input('special');

      expect(_h.count()).toEqual({
        all: 50,
        hidden: 49,
        visible: 1
      });
    });

  });
});

describe('meta tests', () => {
  test('_mergeObj doesn\'t work when one of both isn\'t an object.', () => {
    const _h = new Holmes({find: '.result'});
    let first = {};
    let second = 'string';
    function merge() {
      _h._mergeObj(first,second);
    }
    expect(merge).toThrowError('One of both arguments isn\'t an object.');
  });
});
<commit_msg>allow input into certain elements<commit_after>const fs = require('fs');

const Holmes = require('../js/holmes.js');

/**
 * Set the html stub
 */
const stub = fs.readFileSync('./__tests__/stub.html').toString();

function setStub() {
  document.body.innerHTML = stub;
}

/**
 * Enter a string into this Holmes input
 * @param  {string}      text text to input
 * @param  {HTMLElement} element to input in (optional)
 * @return {Promise}     resolves when the input has been doen
 */
function input(text, input) {
  let holmesInput = document.getElementById('search');
  if (input) {
    holmesInput = input;
  }
  return new Promise((resolve, reject) => {
    if (holmesInput instanceof HTMLInputElement) {
      holmesInput.value = text;
    } else {
      holmesInput.textContent = text;
    }
    holmesInput.dispatchEvent(new Event('input'));
    resolve('input dispatched.');
  });
}

describe('Instance-less usage', () => {
  test('throws without options', () => {
    setStub();

    function init() {
      Holmes();
    }

    expect(init).toThrowError('The options need to be given inside an object like this:\nholmes({\n\tfind:".result"\n});\nsee also https://haroen.me/holmes/doc/module-holmes.html');
  });

  test('throws without .find', () => {
    setStub();

    function init() {
      Holmes({});
    }

    expect(init).toThrowError('A find argument is needed. That should be a querySelectorAll for each of the items you want to match individually. You should have something like: \nholmes({\n\tfind:".result"\n});\nsee also https://haroen.me/holmes/doc/module-holmes.html');
  });

  test('doesn\'t throw with .find given', () => {
    setStub();
    Holmes({
      find: '.result'
    });
  });

  // test('throws when .input you can\'t type in is given', () => {
  //   setStub();
  //   function init() {
  //     const qs = '.result';
  //     Holmes({
  //       find: '.result',
  //       input: qs
  //     });
  //
  //     input('bla', document.querySelector(qs));
  //   }
  //
  //   expect(init).toThrowError('The Holmes input was no <input> or contenteditable.');
  // });

  // test('works with contenteditable', () => {
  //   setStub();
  //   Holmes({
  //     find: '.result',
  //     input: '#contenteditable',
  //     class: {
  //       visible: 'visible'
  //     }
  //   });
  //
  //   input('special');
  //
  //   const special = document.getElementById('contains-special');
  //   const notSpecial = document.querySelectorAll('.result:not(#contains-special)');
  //   const notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
  //
  //   expect(special.classList.contains('visible')).toEqual(true);
  //   expect(notSpecial).toEqual(notSpecialHidden);
  // });
});

describe('options', () => {
  test('.visible on initialisation', () => {
    setStub();
    const find = '.result';
    Holmes({
      find,
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    const all = document.querySelectorAll(find);
    const visible = document.querySelectorAll(find + '.visible');

    expect(all).toEqual(visible);
  });

  describe('.minCharacters', () => {
    test('less than minimum amount doesn\'t make anything happen', () => {
      setStub();
      const find = '.result';
      Holmes({
        find,
        instant: true,
        minCharacters: 5,
        class: {
          visible: 'visible'
        }
      });
      //some short input that's surely not in the data
      input('qsdf');

      const all = document.querySelectorAll(find);
      const visible = document.querySelectorAll(find + '.visible');

      expect(all).toEqual(visible);
    });

    test('over minimum does make things happen', () => {
      setStub();
      const find = '.result';
      Holmes({
        find,
        instant: true,
        minCharacters: 5,
        class: {
          visible: 'visible'
        }
      });

      //some short input that's surely not in the data
      input('qsdfg');

      const all = document.querySelectorAll(find);
      const hidden = document.querySelectorAll(find + '.hidden');

      expect(all).toEqual(hidden);
    });
  });

  // describe('.mark', () => {
  //   test('', () => {
  //     setStub();
  //   });
  // });

  describe('callbacks', () => {
    // test('onHidden', () => {
    //   setStub();
    // });
    // test('onVisible', () => {
    //   setStub();
    // });
    // test('onEmpty', () => {
    //   setStub();
    // });
    // test('onFound', () => {
    //   setStub();
    // });
    // test('onInput', () => {
    //   setStub();
    // });
  });

  describe('.placeholder', () => {
    setStub();
    test('gets added', () => {
      Holmes({
        find: '.result',
        instant: true,
        placeholder: 'test'
      });

      const placeholder = document.getElementById('holmes-placeholder');

      expect(placeholder.innerHTML).toEqual('test');
    });

    test('can\'t be added when there\'s no parent of the elements', () => {
      function start() {
        Holmes({
          find: 'html',
          instant: true,
          placeholder: 'impossible'
        });
      }
      expect(start).toThrowError('The Holmes placeholder could\'t be put; the elements had no parent.');
    });

    test('initially hidden', () => {
      setStub();
      Holmes({
        find: '.result',
        instant: true,
        placeholder: 'test'
      });

      const placeholder = document.getElementById('holmes-placeholder');

      expect(placeholder.classList.contains('hidden')).toBe(true);
    });

    test('not hidden when no results', () => {
      setStub();
      Holmes({
        find: '.result',
        instant: true,
        placeholder: 'test'
      });

      const placeholder = document.getElementById('holmes-placeholder');

      return input('definitely not in the list').then(() => {
        expect(placeholder.classList.contains('hidden')).toBe(false);
      });
    });
  });
})

describe('Usage with instance', () => {
  test('Initialisation works', () => {
    setStub();
    const _h = new Holmes({
      find: '.result'
    });

    expect(_h).toBeDefined();
  });

  test('.clear() empties the input and shows everything', () => {
    setStub();
    const _h = new Holmes({
      find: '.result',
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    input('something');

    _h.clear();

    expect(document.getElementById('search').value).toBe('');

    const all = document.querySelectorAll('.result');
    const visible = document.querySelectorAll('.result' + '.visible');

    expect(all).toEqual(visible);

  });

  test('.stop() stops reacting to input', () => {
    setStub();
    const _h = new Holmes({
      find: '.result',
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    _h.stop();

    input('something');

    const all = document.querySelectorAll('.result');
    const visible = document.querySelectorAll('.result' + '.visible');

    expect(all).toEqual(visible);
  });

  test('.start() after .stop() resumes normal activity', () => {
    setStub();
    const _h = new Holmes({
      find: '.result',
      instant: true,
      class: {
        visible: 'visible'
      }
    });

    const special = document.getElementById('contains-special');
    const notSpecial = document.querySelectorAll('.result:not(#contains-special)');
    let notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
    let notSpecialVisible = document.querySelectorAll('.result:not(#contains-special)' + '.visible');

    _h.stop();

    input('special');

    expect(special.classList.contains('visible')).toEqual(true);
    notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
    notSpecialVisible = document.querySelectorAll('.result:not(#contains-special)' + '.visible');
    expect(notSpecial).toEqual(notSpecialVisible);

    _h.start();

    input('special');

    expect(special.classList.contains('visible')).toEqual(true);
    notSpecialHidden = document.querySelectorAll('.result:not(#contains-special)' + '.hidden');
    notSpecialVisible = document.querySelectorAll('.result:not(#contains-special)' + '.visible');
    expect(notSpecial).toEqual(notSpecialHidden);

  });

  describe('.count()', () => {

    test('before input', () => {
      setStub();
      const _h = new Holmes({
        find: '.result',
        instant: true
      });

      expect(_h.count()).toEqual({
        all: 50,
        hidden: 0,
        visible: 50
      });
    });

    test('updates on input', () => {
      setStub();
      const _h = new Holmes({
        find: '.result',
        instant: true
      });

      input('special');

      expect(_h.count()).toEqual({
        all: 50,
        hidden: 49,
        visible: 1
      });
    });

  });
});

describe('meta tests', () => {
  test('_mergeObj doesn\'t work when one of both isn\'t an object.', () => {
    const _h = new Holmes({find: '.result'});
    let first = {};
    let second = 'string';
    function merge() {
      _h._mergeObj(first,second);
    }
    expect(merge).toThrowError('One of both arguments isn\'t an object.');
  });
});
<|endoftext|>