<commit_before>/*
 * Variables' declaration
 */
// Define a list that holds all card's tiles
var tiles = [
    'tile_01.png',
    'tile_01.png',
    'tile_02.png',
    'tile_02.png',
    'tile_03.png',
    'tile_03.png',
    'tile_04.png',
    'tile_04.png',
    'tile_05.png',
    'tile_05.png',
    'tile_06.png',
    'tile_06.png',
    'tile_07.png',
    'tile_07.png',
    'tile_08.png',
    'tile_08.png'
];

// Define variable for the html deck element
var deck = document.getElementById('deck');

// Define variable for the end game modal results
var modalContent = document.getElementById('modal-content');

// Define variable for the revealed cards
var revealedCards = 0;

// Define array to store the values of the cards picked in order to check for matching cards
var matchingCards = [];

// Define variable for the last card tile the user clicked on
var lastCardClicked = -1;

// Define varible that will allow to clear the setInterval
var stopInterval = '';

// Define variable to store the score
var score;

// Define variable to store the moves
var movesCounter;

// Define variables to set the timer counter
var miliseconds = 0, seconds = 0, minutes = 0, time;

// Define a variable for the time counter element
var timeCounter = document.getElementById('mytime');

// Define variable for the restart game glyphicon
var restartBtn = document.getElementById('restart-game').addEventListener('click', startGame);

/*
 * Start the GAME (call)
 */
startGame();
/*
 * @description Start the GAME (function), Display the cards on the page
 */
function startGame() {
    // Set score to zero
    score = 0;

    // Reset number of moves
    movesCounter = 0;
    setMoves(0);

    // Clear and start the time counter
    miliseconds = 0, seconds = 0, minutes = 0;
    clearTimeout(time);
    timer();

    // Clear setInterval timer
    clearInterval(stopInterval);

    // Shuffle list of cards using the provided "shuffle" method below
    tiles = shuffle(tiles);

    // Create new fresh start deck
    deck.innerHTML = '';

    // Clear modal content
    modalContent.innerHTML = '';

    // loop through each card and create its HTML, dinamically add each card's HTML to the page
    for (var index = 0; index < tiles.length; index++) {
        // Define variable for the cards inside tiles elements
        var cards = ' class="card-face" src="img/tile_back.png" alt="card tiles images"';
        // Define variable that will make the card tiles clickable
        var clickable = ' onclick="clickHandler(\'' + tiles[index] + '\', \'' + index + '\', this)"';
        // Define variable that dinamically sets and id for the cards' images
        var cardsId = ' id="check-card' + index + '"';
        // Set HTML elements deck
        deck.innerHTML += '

';
    }
}

/*
 * @description Game logic for handling card tiles functionality
 * @param {string} card tile src element
 * @param {number} element iteration index
 * @param {string} html img element info
 */
function clickHandler(clickable, index, info) {
    // Handle revealed cards allowing to reveal only two cards
    if (revealedCards < 2 && lastCardClicked != index) {
        // Add revealed cards to the array so that allows to perform comparison on them
        matchingCards[revealedCards] = tiles[index];
        // Identify cards
        matchingCards[revealedCards + 2] = info.id;
        // Add revealed card tile and moves
        revealedCards++;
        setMoves(1);
        // Click event should reveal the hidden card's side
        info.src = 'img/' + tiles[index];
        // Perform marching comparisson on selected pair of cards
        if (revealedCards === 2) {
            if (matchingCards[0] === matchingCards[1]) {
                selectNewMatchingPair();
                // Tracking scores
                score++;
                // Check for game over
                if ((tiles.length / 2) <= score) {
                    $('#myModal').modal({ show: false})
                    $('#myModal').modal('show');
                    modalContent.innerHTML += 'Your time: ' + timeCounter.innerHTML + '
Number of Moves: ' + movesCounter + '
Rating: ' + rating;
                    clearTimeout(time);
                }
            } else {
                // Set up a time interval before tiles go back to unrevealed after half a sec
                stopInterval = setInterval(unmatchingCards, 500);
            }
        }
    lastCardClicked = index;
    }
}

/*
 * @description Manage unmatched card tiles
 */
function unmatchingCards() {
    // Reset card tile back to unrevealed card image
    document.getElementById(matchingCards[2]).src = "img/tile_back.png";
    document.getElementById(matchingCards[3]).src = "img/tile_back.png";
    selectNewMatchingPair();
}

/*
 * @description Selec new pair of card tiles to check
 * @description Reset counters and allow selecting new matching pair
 */
function selectNewMatchingPair() {
    // Clear out the value of revealed cards
    revealedCards = 0;
    // Empty the array of cards in order to allow new selecting cards
    matchingCards = [];
    // Avoid repeating selected card
    lastCardClicked = -1;
    // Clear setInterval timer
    clearInterval(stopInterval);
}

/*
 * @description Manage user's moves counter
 */
function setMoves(move) {
    movesCounter += move;
    numMoves = document.getElementById('clicks');
    numMoves.innerHTML = " Moves : " + movesCounter;
    // Update stars rating (call)
    updateStars(movesCounter);
}

/*
 * @description Manage stars rating counter
 * @param {number} from setMoves() number of user card tile clicks
 */
function updateStars(counter){
    ratingStars = document.getElementById('stars');
    star = '';
    ratingStars.innerHTML = star + star + star;
    rating = 'High';
    // Logic in order to show a specific number of stars
    if (counter >= 10 && counter <= 14) {
        ratingStars.innerHTML = star + star;
        rating = 'Medium';
    } else if (counter > 14) {
        ratingStars.innerHTML = star;
        rating = 'Low';
    }
}

/*
 * @description Manage timer counter
 */
function addTime() {
    seconds++;
    // Logic to update minutes every 60 seconds
    if (seconds >= 60) {
        seconds = 0;
        minutes++;
    }
    // Logic to add leading zeros
    timeCounter.textContent = (minutes ? (minutes > 9 ? minutes : '0' + minutes) : '00') + ':' + (seconds > 9 ? seconds : '0' + seconds);
    timer();
}

/*
 * @description Add seconds to the timer counter
 */
function timer() {
    // Adding second to time
    time = setTimeout(addTime, 1000);
}

/*
 * @description Provided function that manage randomly shuffling card tiles
 *  Shuffle function from http://stackoverflow.com/a/2450976
 * @returns {array/list} randomly shuffled card tiles
 */
function shuffle(tiles) {
    var currentIndex = tiles.length, temporaryValue, randomIndex;

    // While there remain elements to shuffle...
    while (currentIndex !== 0) {
        // Pick a remaining element...
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex -= 1;
        // And swap it with the current element.
        temporaryValue = tiles[currentIndex];
        tiles[currentIndex] = tiles[randomIndex];
        tiles[randomIndex] = temporaryValue;
    }
    return tiles;
}<commit_msg>Update custom.js file<commit_after>/*
 * Variables' declaration
 */
// Define a list that holds all card's tiles
var tiles = [
    'tile_01.png',
    'tile_01.png',
    'tile_02.png',
    'tile_02.png',
    'tile_03.png',
    'tile_03.png',
    'tile_04.png',
    'tile_04.png',
    'tile_05.png',
    'tile_05.png',
    'tile_06.png',
    'tile_06.png',
    'tile_07.png',
    'tile_07.png',
    'tile_08.png',
    'tile_08.png'
];

// Define variable for the html deck element
var deck = document.getElementById('deck');

// Define variable for the end game modal results
var modalContent = document.getElementById('modal-content');

// Define variable for the revealed cards
var revealedCards = 0;

// Define array to store the values of the cards picked in order to check for matching cards
var matchingCards = [];

// Define variable for the last card tile the user clicked on
var lastCardClicked = -1;

// Define varible that will allow to clear the setInterval
var stopInterval = '';

// Define variable to store the score
var score;

// Define variable to store the moves
var movesCounter;

// Define variables to set the timer counter
var miliseconds = 0, seconds = 0, minutes = 0, time;

// Define a variable for the time counter element
var timeCounter = document.getElementById('mytime');

// Define variable for the restart game glyphicon
var restartBtn = document.getElementById('restart-game').addEventListener('click', startGame);

/*
 * Start the GAME (call)
 */
startGame();
/*
 * @description Start the GAME (function), Display the cards on the page
 */
function startGame() {
    // Set score to zero
    score = 0;

    // Reset number of moves
    movesCounter = 0;
    setMoves(0);

    // Clear and start the time counter
    miliseconds = 0, seconds = 0, minutes = 0;
    clearTimeout(time);
    timer();

    // Clear setInterval timer
    clearInterval(stopInterval);

    // Shuffle list of cards using the provided "shuffle" method below
    tiles = shuffle(tiles);

    // Create new fresh start deck
    deck.innerHTML = '';

    // Clear modal content
    modalContent.innerHTML = '';

    // loop through each card and create its HTML, dinamically add each card's HTML to the page
    for (var index = 0; index < tiles.length; index++) {
        // Define variable for the cards inside tiles elements
        //var cards = ' class="card-face" src="img/' + tiles[index] + '" alt="card tiles images"';
        var cards = ' class="card-face" src="img/tile_back.png" alt="card tiles images"';
        // Define variable that will make the card tiles clickable
        var clickable = ' onclick="clickHandler(\'' + tiles[index] + '\', \'' + index + '\', this)"';
        // Define variable that dinamically sets and id for the cards' images
        var cardsId = ' id="check-card' + index + '"';
        // Set HTML elements deck
        deck.innerHTML += '

';
    }
}

/*
 * @description Game logic for handling card tiles functionality
 * @param {string} card tile src element
 * @param {number} element iteration index
 * @param {string} html img element info
 */
function clickHandler(clickable, index, info) {
    // Handle revealed cards allowing to reveal only two cards
    if (revealedCards < 2 && lastCardClicked != index) {
        // Add revealed cards to the array so that allows to perform comparison on them
        matchingCards[revealedCards] = tiles[index];
        // Identify cards
        matchingCards[revealedCards + 2] = info.id;
        // Add revealed card tile and moves
        revealedCards++;
        setMoves(1);
        // Click event should reveal the hidden card's side
        info.src = 'img/' + tiles[index];
        // Perform marching comparisson on selected pair of cards
        if (revealedCards === 2) {
            if (matchingCards[0] === matchingCards[1]) {
                //selectNewMatchingPair();
                // Tracking scores
                score++;
                // Make card tiles unclickable
                unmatchingCards();
                // Check for game over
                if ((tiles.length / 2) <= score) {
                    $('#myModal').modal({ show: false})
                    $('#myModal').modal('show');
                    modalContent.innerHTML += 'Your time: ' + timeCounter.innerHTML + '
Number of Moves: ' + movesCounter + '
Rating: ' + rating;
                    clearTimeout(time);
                }
                //selectNewMatchingPair();
            } else {
                // Set up a time interval before tiles go back to unrevealed after half a sec
                stopInterval = setInterval(unrevealedCards, 500);
            }
        }
    lastCardClicked = index;
    }
}

/*
 * @description Manage matched card tiles
 */
function unmatchingCards() {
    //console.log("INSIDE unmatchingCards");
    // Remove card tile onclick attribute
    document.getElementById(matchingCards[2]).removeAttribute('onclick');
    document.getElementById(matchingCards[3]).removeAttribute('onclick');
    document.getElementById(matchingCards[2]).setAttribute('class', 'card-matched');
    document.getElementById(matchingCards[3]).setAttribute('class', 'card-matched');
    selectNewMatchingPair();
}

/*
 * @description Manage revealed card tiles
 */
function unrevealedCards() {
    // Reset card tile back to unrevealed card image
    document.getElementById(matchingCards[2]).src = "img/tile_back.png";
    document.getElementById(matchingCards[3]).src = "img/tile_back.png";
    selectNewMatchingPair();
}

/*
 * @description Selec new pair of card tiles to check
 * @description Reset counters and allow selecting new matching pair
 */
function selectNewMatchingPair() {
    // Clear out the value of revealed cards
    revealedCards = 0;
    // Empty the array of cards in order to allow new selecting cards
    matchingCards = [];
    // Avoid repeating selected card
    lastCardClicked = -1;
    // Clear setInterval timer
    clearInterval(stopInterval);
}

/*
 * @description Manage user's moves counter
 */
function setMoves(move) {
    movesCounter += move;
    numMoves = document.getElementById('clicks');
    numMoves.innerHTML = " Moves : " + movesCounter;
    // Update stars rating (call)
    updateStars(movesCounter);
}

/*
 * @description Manage stars rating counter
 * @param {number} from setMoves() number of user card tile clicks
 */
function updateStars(counter){
    ratingStars = document.getElementById('stars');
    star = '';
    ratingStars.innerHTML = star + star + star;
    rating = 'High';
    // Logic in order to show a specific number of stars
    if (counter >= 10 && counter <= 14) {
        ratingStars.innerHTML = star + star;
        rating = 'Medium';
    } else if (counter > 14) {
        ratingStars.innerHTML = star;
        rating = 'Low';
    }
}

/*
 * @description Manage timer counter
 */
function addTime() {
    seconds++;
    // Logic to update minutes every 60 seconds
    if (seconds >= 60) {
        seconds = 0;
        minutes++;
    }
    // Logic to add leading zeros
    timeCounter.textContent = (minutes ? (minutes > 9 ? minutes : '0' + minutes) : '00') + ':' + (seconds > 9 ? seconds : '0' + seconds);
    timer();
}

/*
 * @description Add seconds to the timer counter
 */
function timer() {
    // Adding second to time
    time = setTimeout(addTime, 1000);
}

/*
 * @description Provided function that manage randomly shuffling card tiles
 *  Shuffle function from http://stackoverflow.com/a/2450976
 * @returns {array/list} randomly shuffled card tiles
 */
function shuffle(tiles) {
    var currentIndex = tiles.length, temporaryValue, randomIndex;

    // While there remain elements to shuffle...
    while (currentIndex !== 0) {
        // Pick a remaining element...
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex -= 1;
        // And swap it with the current element.
        temporaryValue = tiles[currentIndex];
        tiles[currentIndex] = tiles[randomIndex];
        tiles[randomIndex] = temporaryValue;
    }
    return tiles;
}<|endoftext|>


<commit_before>// reference: https://docs.google.com/spreadsheets/d/1rjZZg19gH4ST3XFfPaUmvgC90ACKgRw8x-MgoozkLfI/edit#gid=0

function shockingUpdate(inputs) {

	var baseline         = getBaseline();
	var gen_production   = baseline['gen_production'];
	var gen_emissions    = baseline['gen_emissions'];
	var gen_cost         = baseline['gen_cost'];
	var gen_capital_cost = {};

	// annual vehicle fleet emissions in Kilotons of CO2 Equivalent
	var fleet_emissions = {
		'Road': 12688
	}

	// ********************************************
	// Business logic
	// ********************************************

	// -----------------
	// [1] Electric cars
	// -----------------

	// ASSUMPTION: there are essentially 0 electric cars in the current fleet
	var electric_cars = inputs['carNumber'];
	var electric_pct = electric_cars/100;

	fleet_emissions['Road'] = fleet_emissions['Road'] * (1-electric_pct);

	// ASSUMPTION: any uptick in power generation required to meet
	// electric car demand, will be met proportionally by all generation sources
	// ASSUMPTION: typical EV will use 0.2 kWh for 1 km travelled
	// average travel is 12032 km per year
	// fleet size is 3341013
	ev_power_reqts = 12032 * 0.2 * 3341013 * electric_pct;

	// convert kwh to gwh
	ev_power_reqts = ev_power_reqts / 1000000;

	// work out total current generation
	var total_gen = 0;
	for (var key in gen_production) {
		total_gen += gen_production[key];
	}

	var increase_in_power_reqts = 1 + (ev_power_reqts / total_gen);

	// -----------------
	// [2] Solar Houses
	// -----------------
	var solar_houses = inputs['solarNumber'];

	// ASSUMPTION - each household generates 5260 KWh per year
	var solar_production = solar_houses * 5260;
	// convert to Gwh
	solar_production = solar_production / 1000000;

	// reduce proportionally all the other production and emissions
	var decrease_due_to_solar = solar_production / total_gen;
	increase_in_power_reqts -= decrease_due_to_solar;

	// ----------
	// [3] Wind
	// ----------
	////var new_wind = inputs['windNumber'];

	// Based on: Each new wind farm is 200W; 702 GWh per annum;
	// capital cost of $521,344,918

	// first the capital cost, that's easy:
	////gen_capital_cost['Wind'] = new_wind * 521344918;

	// new wind capacity
	////var new_wind_capacity = new_wind * 702;
	// new wind capacity gives us associated production, emissions, and running costs

	/*if (new_wind > 0) {
		var new_wind_ratio = new_wind_capacity / gen_production['Wind'];
	}

	gen_production['Wind'] += new_wind_capacity; */

	// TODO: finally reduce the other (hopefully less polluting) generation

	// apply the factor to the production
	for (var key in gen_production) {
		gen_production[key] = gen_production[key] * increase_in_power_reqts;
	}

	// now add in the solar
	gen_production['Solar'] = solar_production;

	for (var key in gen_emissions) {
		gen_emissions[key] = gen_emissions[key] * increase_in_power_reqts;
	}

	// now apply that factor to the emissions - emissions from EV power requirements
	for (var key in gen_emissions) {
		gen_emissions[key] = gen_emissions[key] * increase_in_power_reqts;
	}

	// also apply the same factor to the generation cost
	for (var key in gen_cost) {
		gen_cost[key] = gen_cost[key] * increase_in_power_reqts;
	}

	// ********************************************
	// Output
	// ********************************************
	var result = {
		'gen_production': gen_production,
		'gen_emissions': gen_emissions,
		'gen_cost': gen_cost,
		'gen_capital_cost': gen_capital_cost,
		'fleet_emissions': fleet_emissions
	}

	return result;
}

function getBaseline() {

	// ********************************************
	// Baseline data : 2013/2014
	// ********************************************

	// annual generation in Gigawatt Hours
	var gen_production = {
		'Hydro': 24095,
		'Geothermal': 6487,
		'Wind': 2187,
		'Coal': 1832,
		'Gas': 6626,
    'Solar': 0
	}

	// annual generation emissions in Kilotons of CO2 Equivalent
	var gen_emissions = {
		'Hydro': 0,
		'Geothermal': 847.32,
		'Wind': 0,
		'Coal': 1222.2,
		'Gas': 3405.51
	}

	// annual generation costs in $NZ per year (I think)
	var gen_cost = {
		'Hydro': 53872180,
		'Geothermal': 59535000,
		'Wind': 43484000,
		'Coal': 34177200,
		'Gas': 161044192
	}

	var result = {
		'gen_production': gen_production,
		'gen_emissions': gen_emissions,
		'gen_cost': gen_cost,
	}

	return result;
}

<commit_msg>rounding on costs<commit_after>// reference: https://docs.google.com/spreadsheets/d/1rjZZg19gH4ST3XFfPaUmvgC90ACKgRw8x-MgoozkLfI/edit#gid=0

function shockingUpdate(inputs) {

	var baseline         = getBaseline();
	var gen_production   = baseline['gen_production'];
	var gen_emissions    = baseline['gen_emissions'];
	var gen_cost         = baseline['gen_cost'];
	var gen_capital_cost = {};

	// annual vehicle fleet emissions in Kilotons of CO2 Equivalent
	var fleet_emissions = {
		'Road': 12688
	}

	// ********************************************
	// Business logic
	// ********************************************

	// -----------------
	// [1] Electric cars
	// -----------------

	// ASSUMPTION: there are essentially 0 electric cars in the current fleet
	var electric_cars = inputs['carNumber'];
	var electric_pct = electric_cars/100;

	fleet_emissions['Road'] = fleet_emissions['Road'] * (1-electric_pct);

	// ASSUMPTION: any uptick in power generation required to meet
	// electric car demand, will be met proportionally by all generation sources
	// ASSUMPTION: typical EV will use 0.2 kWh for 1 km travelled
	// average travel is 12032 km per year
	// fleet size is 3341013
	ev_power_reqts = 12032 * 0.2 * 3341013 * electric_pct;

	// convert kwh to gwh
	ev_power_reqts = ev_power_reqts / 1000000;

	// work out total current generation
	var total_gen = 0;
	for (var key in gen_production) {
		total_gen += gen_production[key];
	}

	var increase_in_power_reqts = 1 + (ev_power_reqts / total_gen);

	// -----------------
	// [2] Solar Houses
	// -----------------
	var solar_houses = inputs['solarNumber'];

	// ASSUMPTION - each household generates 5260 KWh per year
	var solar_production = solar_houses * 5260;
	// convert to Gwh
	solar_production = solar_production / 1000000;

	// reduce proportionally all the other production and emissions
	var decrease_due_to_solar = solar_production / total_gen;
	increase_in_power_reqts -= decrease_due_to_solar;

	// ----------
	// [3] Wind
	// ----------
	////var new_wind = inputs['windNumber'];

	// Based on: Each new wind farm is 200W; 702 GWh per annum;
	// capital cost of $521,344,918

	// first the capital cost, that's easy:
	////gen_capital_cost['Wind'] = new_wind * 521344918;

	// new wind capacity
	////var new_wind_capacity = new_wind * 702;
	// new wind capacity gives us associated production, emissions, and running costs

	/*if (new_wind > 0) {
		var new_wind_ratio = new_wind_capacity / gen_production['Wind'];
	}

	gen_production['Wind'] += new_wind_capacity; */

	// TODO: finally reduce the other (hopefully less polluting) generation

	// apply the factor to the production
	for (var key in gen_production) {
		gen_production[key] = gen_production[key] * increase_in_power_reqts;
	}

	// now add in the solar
	gen_production['Solar'] = solar_production;

	for (var key in gen_emissions) {
		gen_emissions[key] = gen_emissions[key] * increase_in_power_reqts;
	}

	// now apply that factor to the emissions - emissions from EV power requirements
	for (var key in gen_emissions) {
		gen_emissions[key] = gen_emissions[key] * increase_in_power_reqts;
	}

	// also apply the same factor to the generation cost
	for (var key in gen_cost) {
		gen_cost[key] = gen_cost[key] * increase_in_power_reqts;
	}

	// ********************************************
	// Data tidying
	// ********************************************

	// convert cost from dollars to millions
	for (var key in gen_cost) {
		gen_cost[key] = gen_cost[key] / 1000000;
	}

	// ********************************************
	// Output
	// ********************************************
	var result = {
		'gen_production': gen_production,
		'gen_emissions': gen_emissions,
		'gen_cost': gen_cost,
		'gen_capital_cost': gen_capital_cost,
		'fleet_emissions': fleet_emissions
	}

	return result;
}

function getBaseline() {

	// ********************************************
	// Baseline data : 2013/2014
	// ********************************************

	// annual generation in Gigawatt Hours
	var gen_production = {
		'Hydro': 24095,
		'Geothermal': 6487,
		'Wind': 2187,
		'Coal': 1832,
		'Gas': 6626,
    'Solar': 0
	}

	// annual generation emissions in Kilotons of CO2 Equivalent
	var gen_emissions = {
		'Hydro': 0,
		'Geothermal': 847.32,
		'Wind': 0,
		'Coal': 1222.2,
		'Gas': 3405.51
	}

	// annual generation costs in $NZ per year (I think)
	var gen_cost = {
		'Hydro': 53872180,
		'Geothermal': 59535000,
		'Wind': 43484000,
		'Coal': 34177200,
		'Gas': 161044192
	}

	var result = {
		'gen_production': gen_production,
		'gen_emissions': gen_emissions,
		'gen_cost': gen_cost,
	}

	return result;
}

<|endoftext|>


<commit_before>if (typeof Cryptodog === 'undefined') {
    Cryptodog = function () { };
}

Cryptodog.UI = {
    // Set version number in UI.
    setVersion: function () {
        $('#version').text(Cryptodog.version);
    },

    // Handles login failures.
    loginFail: function (message) {
        $('#loginInfo').text(message);
        $('#bubble')
            .animate({ left: '+=5px' }, 130)
            .animate({ left: '-=10px' }, 130)
            .animate({ left: '+=5px' }, 130);
    },

    // Handle detected new keys.
    removeAuthAndWarn: function (nickname) {
        var buddy = Cryptodog.buddies[nickname];
        buddy.updateAuth(false);
        dialog.showErrorAKE(nickname);
    },

    logout: function () {
        document.title = 'Cryptodog';

        $('#loginInfo').text(Cryptodog.locale['loginMessage']['thankYouUsing']);
        $('#conversationInfo,#optionButtons').fadeOut();
        $('#header').animate({ 'background-color': 'transparent' });
        $('.logo').animate({ margin: '-5px 5px 0 5px' });
        buddyList.cleanUp();

        $('#userInput').fadeOut(function () {
            $('#logoText').fadeIn();
            $('#footer').animate({ height: 14 });

            $('#conversationWrapper').fadeOut(function () {
                $('#info,#loginOptions,#version,#loginInfo').fadeIn();

                $('#login').fadeIn(200, function () {
                    $('#login').css({ opacity: 1 });
                    $('#conversationName').select();
                    $('#conversationName,#nickname').removeAttr('readonly');
                    $('#loginSubmit').removeAttr('readonly');
                });

                $('#dialogBoxClose').click();
                $('#conversationWindow').html('');
            });
        });
    },

    // Open base64 Data URI in new window.
    openDataInNewWindow: function (url) {
        var win = window.open();
        var image = new Image();
        image.src = url;
        win.document.write(image.outerHTML);
        win.focus();
    },

    addDataLinks: function (message) {
        // Make sure the string is a legitimate data URI, and is of an accepted image format.
        var re = /data:image\/(png|jpeg|gif);(charset=[\w-]+|base64)?,\S+/gi;
        return message.replace(re, '[Embedded image]');
    },

    // Convert message URLs to links. Used internally.
    addLinks: function (message) {
        // Handle image data URIs gracefully:
        message = Cryptodog.UI.addDataLinks(message);
        return message.autoLink();
    },

    // Simple text formatting
    stylizeText: function (text) {
        // Disable text formatting in messages that contain links to avoid interference
        let linkPattern = /(https?|ftps?):\/\//gi;

        if (text.match(linkPattern) === null) {
            // Swap ***.+*** for strong and italic text
            let strongItalicPattern = /\*\*\*((?!\s).+)\*\*\*/gi;
            text = text.replace(strongItalicPattern, "$1");

            // Swap **.+** for strong text
            let strongPattern = /\*\*((?!\s).+)\*\*/gi;
            text = text.replace(strongPattern, "$1");

            // Swap *.+* for italics
            let italicPattern = /\*((?!\s).+)\*/gi;
            text = text.replace(italicPattern, "$1");
        }
        return text;
    },

    // Default emoticons (Unicode) - also in lang/emojis/unicode.json
    emoticons: [
        {
            data: '😢',
            regex: /(\s|^)(:|(=))-?\'\((?=(\s|$))/gi
        }, // :'( - Cry
        {
            data: '😕',
            regex: /(\s|^)(:|(=))-?(\/|s)(?=(\s|$))/gi
        }, // :/ - Unsure
        {
            data: '🐱',
            regex: /(\s|^)(:|(=))-?3(?=(\s|$))/gi
        }, // :3 - Cat face
        {
            data: '😮',
            regex: /(\s|^)(:|(=))-?o(?=(\s|$))/gi
        }, // :O - Shock
        {
            data: '😄',
            regex: /(\s|^)(:|(=))-?D(?=(\s|$))/gi
        }, // :D - Grin
        {
            data: '☹',
            regex: /(\s|^)(:|(=))-?\((?=(\s|$))/gi
        }, // :( - Sad
        {
            data: '😊',
            regex: /(\s|^)(:|(=))-?\)(?=(\s|$))/gi
        }, // :) - Happy
        {
            data: '😛',
            regex: /(\s|^)(:|(=))-?p(?=(\s|$))/gi
        }, // :P - Tongue
        {
            data: '😶',
            regex: /(\s|^)(:|(=))-?x\b(?=(\s|$))/gi
        }, // :x - Shut
        {
            data: '😉',
            regex: /(\s|^);-?\)(?=(\s|$))/gi
        }, // ;) - Wink
        {
            data: '😜',
            regex: /(\s|^);-?\p(?=(\s|$))/gi
        }, // ;P - Winky Tongue
        {
            data: '❤',
            regex: /(\s|^)\&lt\;3\b(?=(\s|$))/g
        } // <3 - Heart
    ],

    // Convert text emoticons to graphical emoticons.
    addEmoticons: function (message) {
        for (var i = 0; i < Cryptodog.UI.emoticons.length; i++) {
            var e = Cryptodog.UI.emoticons[i];
            message = message.replace(e.regex, ' ' + e.data + '');
        }
        return message;
    },

    setEmoticonPack: function (packId) {
        $.getJSON('lang/emojis/' + packId + '.json', function (emojiJSON) {
            console.log("Loaded emoji pack '" + emojiJSON.name + "'");

            if (emojiJSON.type !== 'text') {
                console.error('Non-text emoji sets are not supported right now.');
                return;
            }

            Cryptodog.UI.emoticons = [];
            emojiJSON.data.forEach(function (emoji) {
                console.log('Started loading ' + emoji.name);
                var regex = new RegExp(emoji.regex, emoji.regexflags);
                Cryptodog.UI.emoticons.push({ data: emoji.data, regex: regex });
            });
        });
    },

    /*
	-------------------
	USER INTERFACE BINDINGS
	-------------------
    */
    userInterfaceBindings: function () {
        $('#buddyWhitelist').click(function () {
            if (Cryptodog.buddyWhitelistEnabled) {
                $(this).attr('src', 'img/icons/users.svg');
                $(this).attr('data-utip', 'Buddy whitelist: off');
            } else {
                $(this).attr('src', 'img/icons/lock.svg');
                $(this).attr('data-utip', 'Buddy whitelist: on');
            }

            $(this).mouseenter();
            Cryptodog.toggleBuddyWhitelist();
        });

        // Audio notifications toggle button
        $('#audioToggle').click(function () {
            if (Cryptodog.allowSoundNotifications) {
                Cryptodog.allowSoundNotifications = false;
                Cryptodog.storage.setItem('audioNotifications', 'false');
                $('#audioToggle').attr('data-utip', 'Audio notifications: off');
                $('#audioToggle').attr('src', 'img/icons/volume-mute.svg');
            } else {
                Cryptodog.allowSoundNotifications = true;
                Cryptodog.storage.setItem('audioNotifications', 'true');
                $('#audioToggle').attr('data-utip', 'Audio notifications: on');
                $('#audioToggle').attr('src', 'img/icons/volume-medium.svg');
            }

            $('#audioToggle').mouseenter();
        });

        // Status button.
        $('#status').click(function () {
            var $this = $(this);
            if ($this.attr('src') === 'img/icons/checkmark.svg') {
                $this.attr('src', 'img/icons/cross.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['statusAway']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['statusAway']);
                $this.mouseenter();
                Cryptodog.me.status = 'away';
            } else {
                $this.attr('src', 'img/icons/checkmark.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['statusAvailable']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['statusAvailable']);
                $this.mouseenter();
                Cryptodog.me.status = 'online';
            }
            meta.sendStatus(Cryptodog.me.status);
        });

        // My info button.
        $('#myInfo').click(function () {
            dialog.showMyInfo(Cryptodog.me);
        });

        // Desktop notifications button.
        $('#notifications').click(function () {
            var $this = $(this);

            if ($this.attr('src') === 'img/icons/bubble2.svg') {
                $this.attr('src', 'img/icons/bubble.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['desktopNotificationsOn']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['desktopNotificationsOn']);
                $this.mouseenter();

                Cryptodog.desktopNotifications = true;
                Cryptodog.storage.setItem('desktopNotifications', 'true');

                var notifStatus = Notification.permission;
                if (notifStatus == 'denied') {
                    // notifications supported but not enabled
                    Notification.requestPermission();

                    // check if user actually accepted
                    if (Notification.permission == 'denied') {
                        Cryptodog.desktopNotifications = false;
                        Cryptodog.storage.setItem('desktopNotifications', 'false');
                    }
                } else if (notifStatus == 'unknown') {
                    // browser doesn't support desktop notifications
                    alert("It looks like your browser doesn't support desktop notifications.");

                    $this.attr('src', 'img/icons/bubble2.svg');
                    $this.attr('title', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                    $this.attr('data-utip', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                    $this.mouseenter();

                    Cryptodog.desktopNotifications = false;
                    Cryptodog.storage.setItem('desktopNotifications', 'false');
                }
            } else {
                $this.attr('src', 'img/icons/bubble2.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                $this.mouseenter();

                Cryptodog.desktopNotifications = false;
                Cryptodog.storage.setItem('desktopNotifications', 'false');
            }
        });

        // Logout button.
        $('#logout').click(function () {
            Cryptodog.logout();
        });

        // Language selector.
        $('#languageSelect').click(function () {
            $('#customServerDialog').hide();
            $('#languages li').css({ color: '#FFF', 'font-weight': 'normal' });

            $('[data-locale=' + Cryptodog.locale['language'] + ']').css({
                color: '#CCC',
                'font-weight': 'bold'
            });

            $('#footer').animate({ height: 190 }, function () {
                $('#languages').fadeIn();
            });

            $('#languages li').click(function () {
                var lang = $(this).attr('data-locale');
                $('#languages').fadeOut(200, function () {
                    Cryptodog.locale.set(lang, true);
                    Cryptodog.storage.setItem('language', lang);
                    $('#footer').animate({ height: 14 });
                });
            });
        });

        // Login form.
        $('#conversationName').click(function () {
            $(this).select();
        });
        $('#nickname').click(function () {
            $(this).select();
        });

        $(document).bind('mousedown', function (e) {
            if (!$(e.target).parents('.buddyMenu').length > 0) {
                $('.buddyMenu').hide();
                $('.buddy.active').removeClass('active');
            }
        });
    },

    /*
	-------------------
	WINDOW EVENT BINDINGS
	-------------------
    */
    windowEventBindings: function () {
        $(window).ready(function () {
            // Initialize language settings.
            Cryptodog.storage.getItem('language', function (key) {
                if (key) {
                    Cryptodog.locale.set(key, true);
                } else {
                    Cryptodog.locale.set(window.navigator.language.toLowerCase());
                }
            });

            // Load custom servers.
            Cryptodog.storage.getItem('customServers', function (key) {
                if (key) {
                    $('#customServerSelector').empty();
                    var servers = $.parseJSON(key);

                    $.each(servers, function (name) {
                        $('#customServerSelector').append(
                            Mustache.render(Cryptodog.templates['customServer'], {
                                name: name,
                                domain: servers[name]['domain'],
                                xmpp: servers[name]['xmpp'],
                                relay: servers[name]['relay']
                            })
                        );
                    });
                }
            });

            // Load nickname settings.
            Cryptodog.storage.getItem('nickname', function (key) {
                if (key) {
                    $('#nickname').animate({ color: 'transparent' }, function () {
                        $(this).val(key);
                        $(this).animate({ color: '#FFF' });
                    });
                }
            });

            // Load notification settings.
            window.setTimeout(function () {
                Cryptodog.storage.getItem('desktopNotifications', function (key) {
                    if (key === 'true') {
                        $('#notifications').click();
                        $('#utip').hide();
                    }
                });
                Cryptodog.storage.getItem('audioNotifications', function (key) {
                    if (key === 'true') {
                        $('#audioToggle').click();
                    }
                });
            }, 800);
        });

        // When the window/tab is not selected, set `windowFocus` to false.
        // `windowFocus` is used to know when to show desktop notifications.
        $(window).blur(function () {
            Cryptodog.me.windowFocus = false;
        });

        // On window focus, select text input field automatically if we are chatting.
        // Also set `windowFocus` to true.
        $(window).focus(function () {
            Cryptodog.me.windowFocus = true;
            Cryptodog.newMessageCount();

            if (Cryptodog.me.currentBuddy) {
                $('#userInputText').focus();
            }
        });

        // Prevent accidental window close.
        $(window).bind('beforeunload', function () {
            if (Object.keys(Cryptodog.buddies).length) {
                return Cryptodog.locale['loginMessage']['thankYouUsing'];
            }
        });

        // Logout on browser close.
        window.onunload = function () {
            net.disconnect();
        };

        // Determine whether we are showing a top margin
        // Depending on window size
        $(window).resize(function () {
            if ($(window).height() < 650) {
                $('#bubble').css('margin-top', '0');
            } else {
                $('#bubble').css('margin-top', '2%');
            }
        });
        $(window).resize();
    },

    show: function () {
        // Show main window.
        $('#bubble').show();
    }
};
<commit_msg>Delete removeAuthAndWarn<commit_after>if (typeof Cryptodog === 'undefined') {
    Cryptodog = function () { };
}

Cryptodog.UI = {
    // Set version number in UI.
    setVersion: function () {
        $('#version').text(Cryptodog.version);
    },

    // Handles login failures.
    loginFail: function (message) {
        $('#loginInfo').text(message);
        $('#bubble')
            .animate({ left: '+=5px' }, 130)
            .animate({ left: '-=10px' }, 130)
            .animate({ left: '+=5px' }, 130);
    },

    logout: function () {
        document.title = 'Cryptodog';

        $('#loginInfo').text(Cryptodog.locale['loginMessage']['thankYouUsing']);
        $('#conversationInfo,#optionButtons').fadeOut();
        $('#header').animate({ 'background-color': 'transparent' });
        $('.logo').animate({ margin: '-5px 5px 0 5px' });
        buddyList.cleanUp();

        $('#userInput').fadeOut(function () {
            $('#logoText').fadeIn();
            $('#footer').animate({ height: 14 });

            $('#conversationWrapper').fadeOut(function () {
                $('#info,#loginOptions,#version,#loginInfo').fadeIn();

                $('#login').fadeIn(200, function () {
                    $('#login').css({ opacity: 1 });
                    $('#conversationName').select();
                    $('#conversationName,#nickname').removeAttr('readonly');
                    $('#loginSubmit').removeAttr('readonly');
                });

                $('#dialogBoxClose').click();
                $('#conversationWindow').html('');
            });
        });
    },

    // Open base64 Data URI in new window.
    openDataInNewWindow: function (url) {
        var win = window.open();
        var image = new Image();
        image.src = url;
        win.document.write(image.outerHTML);
        win.focus();
    },

    addDataLinks: function (message) {
        // Make sure the string is a legitimate data URI, and is of an accepted image format.
        var re = /data:image\/(png|jpeg|gif);(charset=[\w-]+|base64)?,\S+/gi;
        return message.replace(re, '[Embedded image]');
    },

    // Convert message URLs to links. Used internally.
    addLinks: function (message) {
        // Handle image data URIs gracefully:
        message = Cryptodog.UI.addDataLinks(message);
        return message.autoLink();
    },

    // Simple text formatting
    stylizeText: function (text) {
        // Disable text formatting in messages that contain links to avoid interference
        let linkPattern = /(https?|ftps?):\/\//gi;

        if (text.match(linkPattern) === null) {
            // Swap ***.+*** for strong and italic text
            let strongItalicPattern = /\*\*\*((?!\s).+)\*\*\*/gi;
            text = text.replace(strongItalicPattern, "$1");

            // Swap **.+** for strong text
            let strongPattern = /\*\*((?!\s).+)\*\*/gi;
            text = text.replace(strongPattern, "$1");

            // Swap *.+* for italics
            let italicPattern = /\*((?!\s).+)\*/gi;
            text = text.replace(italicPattern, "$1");
        }
        return text;
    },

    // Default emoticons (Unicode) - also in lang/emojis/unicode.json
    emoticons: [
        {
            data: '😢',
            regex: /(\s|^)(:|(=))-?\'\((?=(\s|$))/gi
        }, // :'( - Cry
        {
            data: '😕',
            regex: /(\s|^)(:|(=))-?(\/|s)(?=(\s|$))/gi
        }, // :/ - Unsure
        {
            data: '🐱',
            regex: /(\s|^)(:|(=))-?3(?=(\s|$))/gi
        }, // :3 - Cat face
        {
            data: '😮',
            regex: /(\s|^)(:|(=))-?o(?=(\s|$))/gi
        }, // :O - Shock
        {
            data: '😄',
            regex: /(\s|^)(:|(=))-?D(?=(\s|$))/gi
        }, // :D - Grin
        {
            data: '☹',
            regex: /(\s|^)(:|(=))-?\((?=(\s|$))/gi
        }, // :( - Sad
        {
            data: '😊',
            regex: /(\s|^)(:|(=))-?\)(?=(\s|$))/gi
        }, // :) - Happy
        {
            data: '😛',
            regex: /(\s|^)(:|(=))-?p(?=(\s|$))/gi
        }, // :P - Tongue
        {
            data: '😶',
            regex: /(\s|^)(:|(=))-?x\b(?=(\s|$))/gi
        }, // :x - Shut
        {
            data: '😉',
            regex: /(\s|^);-?\)(?=(\s|$))/gi
        }, // ;) - Wink
        {
            data: '😜',
            regex: /(\s|^);-?\p(?=(\s|$))/gi
        }, // ;P - Winky Tongue
        {
            data: '❤',
            regex: /(\s|^)\&lt\;3\b(?=(\s|$))/g
        } // <3 - Heart
    ],

    // Convert text emoticons to graphical emoticons.
    addEmoticons: function (message) {
        for (var i = 0; i < Cryptodog.UI.emoticons.length; i++) {
            var e = Cryptodog.UI.emoticons[i];
            message = message.replace(e.regex, ' ' + e.data + '');
        }
        return message;
    },

    setEmoticonPack: function (packId) {
        $.getJSON('lang/emojis/' + packId + '.json', function (emojiJSON) {
            console.log("Loaded emoji pack '" + emojiJSON.name + "'");

            if (emojiJSON.type !== 'text') {
                console.error('Non-text emoji sets are not supported right now.');
                return;
            }

            Cryptodog.UI.emoticons = [];
            emojiJSON.data.forEach(function (emoji) {
                console.log('Started loading ' + emoji.name);
                var regex = new RegExp(emoji.regex, emoji.regexflags);
                Cryptodog.UI.emoticons.push({ data: emoji.data, regex: regex });
            });
        });
    },

    /*
	-------------------
	USER INTERFACE BINDINGS
	-------------------
    */
    userInterfaceBindings: function () {
        $('#buddyWhitelist').click(function () {
            if (Cryptodog.buddyWhitelistEnabled) {
                $(this).attr('src', 'img/icons/users.svg');
                $(this).attr('data-utip', 'Buddy whitelist: off');
            } else {
                $(this).attr('src', 'img/icons/lock.svg');
                $(this).attr('data-utip', 'Buddy whitelist: on');
            }

            $(this).mouseenter();
            Cryptodog.toggleBuddyWhitelist();
        });

        // Audio notifications toggle button
        $('#audioToggle').click(function () {
            if (Cryptodog.allowSoundNotifications) {
                Cryptodog.allowSoundNotifications = false;
                Cryptodog.storage.setItem('audioNotifications', 'false');
                $('#audioToggle').attr('data-utip', 'Audio notifications: off');
                $('#audioToggle').attr('src', 'img/icons/volume-mute.svg');
            } else {
                Cryptodog.allowSoundNotifications = true;
                Cryptodog.storage.setItem('audioNotifications', 'true');
                $('#audioToggle').attr('data-utip', 'Audio notifications: on');
                $('#audioToggle').attr('src', 'img/icons/volume-medium.svg');
            }

            $('#audioToggle').mouseenter();
        });

        // Status button.
        $('#status').click(function () {
            var $this = $(this);
            if ($this.attr('src') === 'img/icons/checkmark.svg') {
                $this.attr('src', 'img/icons/cross.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['statusAway']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['statusAway']);
                $this.mouseenter();
                Cryptodog.me.status = 'away';
            } else {
                $this.attr('src', 'img/icons/checkmark.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['statusAvailable']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['statusAvailable']);
                $this.mouseenter();
                Cryptodog.me.status = 'online';
            }
            meta.sendStatus(Cryptodog.me.status);
        });

        // My info button.
        $('#myInfo').click(function () {
            dialog.showMyInfo(Cryptodog.me);
        });

        // Desktop notifications button.
        $('#notifications').click(function () {
            var $this = $(this);

            if ($this.attr('src') === 'img/icons/bubble2.svg') {
                $this.attr('src', 'img/icons/bubble.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['desktopNotificationsOn']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['desktopNotificationsOn']);
                $this.mouseenter();

                Cryptodog.desktopNotifications = true;
                Cryptodog.storage.setItem('desktopNotifications', 'true');

                var notifStatus = Notification.permission;
                if (notifStatus == 'denied') {
                    // notifications supported but not enabled
                    Notification.requestPermission();

                    // check if user actually accepted
                    if (Notification.permission == 'denied') {
                        Cryptodog.desktopNotifications = false;
                        Cryptodog.storage.setItem('desktopNotifications', 'false');
                    }
                } else if (notifStatus == 'unknown') {
                    // browser doesn't support desktop notifications
                    alert("It looks like your browser doesn't support desktop notifications.");

                    $this.attr('src', 'img/icons/bubble2.svg');
                    $this.attr('title', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                    $this.attr('data-utip', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                    $this.mouseenter();

                    Cryptodog.desktopNotifications = false;
                    Cryptodog.storage.setItem('desktopNotifications', 'false');
                }
            } else {
                $this.attr('src', 'img/icons/bubble2.svg');
                $this.attr('title', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                $this.attr('data-utip', Cryptodog.locale['chatWindow']['desktopNotificationsOff']);
                $this.mouseenter();

                Cryptodog.desktopNotifications = false;
                Cryptodog.storage.setItem('desktopNotifications', 'false');
            }
        });

        // Logout button.
        $('#logout').click(function () {
            Cryptodog.logout();
        });

        // Language selector.
        $('#languageSelect').click(function () {
            $('#customServerDialog').hide();
            $('#languages li').css({ color: '#FFF', 'font-weight': 'normal' });

            $('[data-locale=' + Cryptodog.locale['language'] + ']').css({
                color: '#CCC',
                'font-weight': 'bold'
            });

            $('#footer').animate({ height: 190 }, function () {
                $('#languages').fadeIn();
            });

            $('#languages li').click(function () {
                var lang = $(this).attr('data-locale');
                $('#languages').fadeOut(200, function () {
                    Cryptodog.locale.set(lang, true);
                    Cryptodog.storage.setItem('language', lang);
                    $('#footer').animate({ height: 14 });
                });
            });
        });

        // Login form.
        $('#conversationName').click(function () {
            $(this).select();
        });
        $('#nickname').click(function () {
            $(this).select();
        });

        $(document).bind('mousedown', function (e) {
            if (!$(e.target).parents('.buddyMenu').length > 0) {
                $('.buddyMenu').hide();
                $('.buddy.active').removeClass('active');
            }
        });
    },

    /*
	-------------------
	WINDOW EVENT BINDINGS
	-------------------
    */
    windowEventBindings: function () {
        $(window).ready(function () {
            // Initialize language settings.
            Cryptodog.storage.getItem('language', function (key) {
                if (key) {
                    Cryptodog.locale.set(key, true);
                } else {
                    Cryptodog.locale.set(window.navigator.language.toLowerCase());
                }
            });

            // Load custom servers.
            Cryptodog.storage.getItem('customServers', function (key) {
                if (key) {
                    $('#customServerSelector').empty();
                    var servers = $.parseJSON(key);

                    $.each(servers, function (name) {
                        $('#customServerSelector').append(
                            Mustache.render(Cryptodog.templates['customServer'], {
                                name: name,
                                domain: servers[name]['domain'],
                                xmpp: servers[name]['xmpp'],
                                relay: servers[name]['relay']
                            })
                        );
                    });
                }
            });

            // Load nickname settings.
            Cryptodog.storage.getItem('nickname', function (key) {
                if (key) {
                    $('#nickname').animate({ color: 'transparent' }, function () {
                        $(this).val(key);
                        $(this).animate({ color: '#FFF' });
                    });
                }
            });

            // Load notification settings.
            window.setTimeout(function () {
                Cryptodog.storage.getItem('desktopNotifications', function (key) {
                    if (key === 'true') {
                        $('#notifications').click();
                        $('#utip').hide();
                    }
                });
                Cryptodog.storage.getItem('audioNotifications', function (key) {
                    if (key === 'true') {
                        $('#audioToggle').click();
                    }
                });
            }, 800);
        });

        // When the window/tab is not selected, set `windowFocus` to false.
        // `windowFocus` is used to know when to show desktop notifications.
        $(window).blur(function () {
            Cryptodog.me.windowFocus = false;
        });

        // On window focus, select text input field automatically if we are chatting.
        // Also set `windowFocus` to true.
        $(window).focus(function () {
            Cryptodog.me.windowFocus = true;
            Cryptodog.newMessageCount();

            if (Cryptodog.me.currentBuddy) {
                $('#userInputText').focus();
            }
        });

        // Prevent accidental window close.
        $(window).bind('beforeunload', function () {
            if (Object.keys(Cryptodog.buddies).length) {
                return Cryptodog.locale['loginMessage']['thankYouUsing'];
            }
        });

        // Logout on browser close.
        window.onunload = function () {
            net.disconnect();
        };

        // Determine whether we are showing a top margin
        // Depending on window size
        $(window).resize(function () {
            if ($(window).height() < 650) {
                $('#bubble').css('margin-top', '0');
            } else {
                $('#bubble').css('margin-top', '2%');
            }
        });
        $(window).resize();
    },

    show: function () {
        // Show main window.
        $('#bubble').show();
    }
};
<|endoftext|>


<commit_before>// functions related to exporting

pg.export = function() {
	var exportRect;
	var canvas;

	var setup = function() {
		canvas = document.getElementById("paperCanvas");
	};

	var setExportRect = function(rect) {
		exportRect = rect;
	};

	var getExportRect = function() {
		return exportRect;
	};

	var clearExportRect = function() {
		exportRect = null;
	};

	var exportAndPromptImage = function() {
		var fileName = prompt("Name your file", "export");

		if (fileName !== null) {
			pg.hover.clearHoveredItem();
			pg.selection.clearSelection();

			// backup guide layer, then remove it (with all children) before export
			var guideLayer = pg.layer.getGuideLayer();
			var guideLayerBackup = guideLayer.exportJSON();
			guideLayer.remove();
			paper.view.update();

			if(exportRect) {
				pg.view.resetZoom();
				pg.view.resetPan();
				paper.view.update();
				var offsetX = parseInt(canvas.width *0.5) + exportRect.x;
				var offsetY = parseInt(canvas.height *0.5) + exportRect.y;

				var fileNameNoExtension = fileName.split(".png")[0];
				var ctx = canvas.getContext("2d");
				var imgData = ctx.getImageData(offsetX, offsetY, exportRect.width, exportRect.height);

				var $tempCanvas = jQuery('<canvas width="'+exportRect.width+'" height="'+exportRect.height+'" style="position: absolute; z-index: -5;">');

				jQuery('body').append($tempCanvas);

				var context = $tempCanvas[0].getContext("2d");
				context.putImageData(imgData,0,0);
				$tempCanvas[0].toBlob(function(blob) {
					saveAs(blob, fileNameNoExtension+'.png');

					// restore guide layer (with all items) after export
					paper.project.importJSON(guideLayerBackup);
				});

				$tempCanvas.remove();

			} else {
				var fileNameNoExtension = fileName.split(".png")[0];
				canvas.toBlob(function(blob) {
					saveAs(blob, fileNameNoExtension+'.png');

					// restore guide layer (with all items) after export
					paper.project.importJSON(guideLayerBackup);
				});
			}

		}
	};

	var exportAndPromptSVG = function() {
		var fileName = prompt("Name your file", "export");

		if (fileName !== null) {
			pg.hover.clearHoveredItem();
			pg.selection.clearSelection();

			var fileNameNoExtension = fileName.split(".svg")[0];

			// backup guide layer, then remove it (with all children) before export
			var guideLayer = pg.layer.getGuideLayer();
			var guideLayerBackup = guideLayer.exportJSON();
			guideLayer.remove();
			paper.view.update();

			// export data, create blob  and save as file on users device
			var exportData = paper.project.exportSVG({ asString: true, bounds: exportRect });
			var blob = new Blob([exportData], {type: "image/svg+xml;charset=" + document.characterSet});
			saveAs(blob, fileNameNoExtension+'.svg');

			// restore guide layer (with all items) after export
			paper.project.importJSON(guideLayerBackup);
		}
	};

	return {
		setup: setup,
		getExportRect: getExportRect,
		setExportRect: setExportRect,
		clearExportRect: clearExportRect,
		exportAndPromptImage: exportAndPromptImage,
		exportAndPromptSVG: exportAndPromptSVG
	};

}();<commit_msg>- fixed bug where the wrong layer was active after exporting, causing newly created items to be assigned to the guide layer instead of the active layer<commit_after>// functions related to exporting

pg.export = function() {
	var exportRect;
	var canvas;

	var setup = function() {
		canvas = document.getElementById("paperCanvas");
	};

	var setExportRect = function(rect) {
		exportRect = rect;
	};

	var getExportRect = function() {
		return exportRect;
	};

	var clearExportRect = function() {
		exportRect = null;
	};

	var exportAndPromptImage = function() {
		var fileName = prompt("Name your file", "export");

		if (fileName !== null) {
			pg.hover.clearHoveredItem();
			pg.selection.clearSelection();
			var activeLayer = pg.layer.getActiveLayer();

			// backup guide layer, then remove it (with all children) before export
			var guideLayer = pg.layer.getGuideLayer();
			var guideLayerBackup = guideLayer.exportJSON();
			guideLayer.remove();
			paper.view.update();

			if(exportRect) {
				pg.view.resetZoom();
				pg.view.resetPan();
				paper.view.update();
				var offsetX = parseInt(canvas.width *0.5) + exportRect.x;
				var offsetY = parseInt(canvas.height *0.5) + exportRect.y;

				var fileNameNoExtension = fileName.split(".png")[0];
				var ctx = canvas.getContext("2d");
				var imgData = ctx.getImageData(offsetX, offsetY, exportRect.width, exportRect.height);

				var $tempCanvas = jQuery('<canvas width="'+exportRect.width+'" height="'+exportRect.height+'" style="position: absolute; z-index: -5;">');

				jQuery('body').append($tempCanvas);

				var context = $tempCanvas[0].getContext("2d");
				context.putImageData(imgData,0,0);
				$tempCanvas[0].toBlob(function(blob) {
					saveAs(blob, fileNameNoExtension+'.png');

					// restore guide layer (with all items) after export
					paper.project.importJSON(guideLayerBackup);

					// then reactivate the active layer
					activeLayer.activate();
				});

				$tempCanvas.remove();

			} else {
				var fileNameNoExtension = fileName.split(".png")[0];
				canvas.toBlob(function(blob) {
					saveAs(blob, fileNameNoExtension+'.png');

					// restore guide layer (with all items) after export
					paper.project.importJSON(guideLayerBackup);

					// then reactivate the active layer
					activeLayer.activate();
				});
			}

		}
	};

	var exportAndPromptSVG = function() {
		var fileName = prompt("Name your file", "export");

		if (fileName !== null) {
			pg.hover.clearHoveredItem();
			pg.selection.clearSelection();

			var activeLayer = pg.layer.getActiveLayer();

			var fileNameNoExtension = fileName.split(".svg")[0];

			// backup guide layer, then remove it (with all children) before export
			var guideLayer = pg.layer.getGuideLayer();
			var guideLayerBackup = guideLayer.exportJSON();
			guideLayer.remove();
			paper.view.update();

			// export data, create blob  and save as file on users device
			var exportData = paper.project.exportSVG({ asString: true, bounds: exportRect });
			var blob = new Blob([exportData], {type: "image/svg+xml;charset=" + document.characterSet});
			saveAs(blob, fileNameNoExtension+'.svg');

			// restore guide layer (with all items) after export
			paper.project.importJSON(guideLayerBackup);

			// then reactivate the active layer
			activeLayer.activate();
		}
	};

	return {
		setup: setup,
		getExportRect: getExportRect,
		setExportRect: setExportRect,
		clearExportRect: clearExportRect,
		exportAndPromptImage: exportAndPromptImage,
		exportAndPromptSVG: exportAndPromptSVG
	};

}();<|endoftext|>


<commit_before>/*
 * Copyright 2017, Joachim Kuebart <joachim.kuebart@gmail.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the
 *      distribution.
 *
 *   3. Neither the name of the copyright holder nor the names of its
 *      contributors may be used to endorse or promote products derived
 *      from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

var GPXMAP = (function () {
'use strict';

var CSS = {
    'DETAILS': 'gpxmap-details',
    'HBOX': 'gpxmap-hbox',
    'SELECT': 'gpxmap-select',
    'TRACK': 'gpxmap-track',
    'VBOX': 'gpxmap-vbox',
    'VIEW': 'gpxmap-view',
};

/**
 * If this looks familiar that's because it's binary search. We find
 * 0 <= i <= array.length such that !pred(i - 1) && pred(i) under the
 * assumption !pred(-1) && pred(length).
 */
function search(array, pred) {
    let le = -1, ri = array.length;
    while (1 + le !== ri) {
        let mi = le + ((ri - le) >> 1);
        if (pred(array[mi])) {
            ri = mi;
        } else {
            le = mi;
        }
    }
    return ri;
}

function yearColour(idx) {
    return ['hsl(',
        170 + 45 * (idx >> 1), ',',
        100, '%,',
        27 * (1 + idx % 2), '%)'
    ].join('');
}

function walkPopup(date, walk) {
    var popup = document.createElement('div');
    var anchor = document.createElement('a');
    var idx = search(walk.dates, d => date <= d);

    if (walk.dates[idx] !== date) {
        console.log('walkPopup', date, walk);
        return;
    }

    popup.textContent =
            [ +date.substr(8, 2)
            , +date.substr(5, 2)
            , +date.substr(0, 4)
            ].join('/') + ' ';
    anchor.setAttribute('href', walk.link);
    anchor.textContent = walk.title;
    popup.appendChild(anchor);
    popup.appendChild(document.createTextNode(
        ' '+ +(walk.distances[idx] / 1000).toFixed(1) +'km'
    ));
    return popup;
}

function gpxmap(id, options) {
    // Create all configured tile layers.
    var tileLayers = options.tileLayers.map(function (layer) {
        return {
            'name': layer.name,
            'tileLayer': L.tileLayer(layer.url, layer.options),
        };
    });

    // Create layers control.
    var layersControl = L.control.layers(null, null, { 'hideSingleBase':true });
    tileLayers.forEach(function (layer) {
        layersControl.addBaseLayer(layer.tileLayer, layer.name);
    });

    // Create the DOM structure for our map.
    var domContainer = L.DomUtil.get(id);
    L.DomUtil.addClass(domContainer, CSS.VBOX);

    // Create map with an initial tile layer and layers control.
    var gpxmap = L.map(L.DomUtil.create('div', CSS.VIEW, domContainer)).
        addControl(L.control.scale()).
        addControl(layersControl).
        addLayer(tileLayers[0].tileLayer);

    var domDetails = L.DomUtil.create('div', CSS.DETAILS, domContainer);

    var hiddenYear = {};
    function trackStyle(hover) {
        return function (props) {
            var year = props.date.substr(0, 4);
            return hiddenYear[year] ? [] : {
                'className': CSS.TRACK,
                'color': yearColour(year - 2011),
                'opacity': hover ? 1 : .8,
                'weight': hover ? 4 : 2,
            };
        };
    }

    // This layer shows walking tracks.
    var walkLayer = L.vectorGrid.protobuf(
        options.url, {
            'getFeatureId': function (walk) { return walk.properties.date; },
            'vectorTileLayerStyles': { '': trackStyle(false) },
        }
    ).addTo(gpxmap);

    // This layer has invisible mouse-responsive tracks.
    var mouseLayer = L.vectorGrid.protobuf(
        options.url, {
            'getFeatureId': function (walk) { return walk.properties.date; },
            'vectorTileLayerStyles': { '': function (props) {
                return hiddenYear[props.date.substr(0, 4)] ? [] : {
                    'opacity': 0,
                    'weight': 20,
                };
            } },
            'interactive': true,
        }
    ).on('mouseover', function (evt) {
        walkLayer.setFeatureStyle(evt.layer.properties.date, trackStyle(true));
    }).on('mouseout', function (evt) {
        walkLayer.resetFeatureStyle(evt.layer.properties.date);
    }).addTo(gpxmap);

    UTIL.load(options.index).then(function (walks) {
        // Collect all years.
        var years = {};
        walks.flatMap(function (walk) { return walk.dates; }).
            forEach(function (date) { years[date.substr(0, 4)] = true; });

        // Build popup from matching index entry.
        mouseLayer.on('click', function (evt) {
            var date = evt.layer.properties.date;
            var idx = search(walks, walk => date < walk.dates[0]) - 1;
            var popup = walkPopup(date, walks[idx]);
            this.closePopup().unbindPopup();
            if (popup) {
                this.bindPopup(popup).openPopup(evt.latlng);
            }
        });

        // Set up a summary pane that reacts when years are toggled.
        var sumPane = Summary.summaryPane(walks, function () {
            var h3 = L.DomUtil.create('h3');

            h3.textContent = options.title;
            L.DomUtil.empty(domDetails);
            domDetails.appendChild(h3);
            domDetails.appendChild(this.render());
        });

        // Create one layer group per year and add to the map.
        Object.keys(years).forEach(function (year) {
            var lg = L.layerGroup().on('add', function () {
                if (hiddenYear[year]) {
                    hiddenYear[year] = false;
                    walkLayer.redraw();
                    mouseLayer.redraw();
                }
            }).on('remove', function () {
                if (!hiddenYear[year]) {
                    hiddenYear[year] = true;
                    walkLayer.redraw();
                    mouseLayer.redraw();
                }
            });
            gpxmap.addLayer(lg);
            layersControl.addOverlay(lg, year);
            sumPane.addLayer(lg, year);
        });

        // Adjust the map's viewport when all GPX tracks are loaded
        var bounds = L.latLngBounds(walks.flatMap(function (walk) {
            return walk.bboxes;
        }).flatMap(function (bbox) {
            var l = bbox.length >> 1;
            return L.GeoJSON.coordsToLatLngs([
                [ bbox[0], bbox[1] ], [ bbox[l], bbox[1 + l] ]
            ]);
        }));
        gpxmap.setMaxBounds(bounds.pad(.05)).fitBounds(bounds);
    });

    return gpxmap;
}

// Export public symbols
return {
    'CSS': CSS,
    'gpxmap': gpxmap,
};
}());
<commit_msg>Our vector tiles are overlays while GridLayer defaults to tilePane.<commit_after>/*
 * Copyright 2017, Joachim Kuebart <joachim.kuebart@gmail.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the
 *      distribution.
 *
 *   3. Neither the name of the copyright holder nor the names of its
 *      contributors may be used to endorse or promote products derived
 *      from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

var GPXMAP = (function () {
'use strict';

var CSS = {
    'DETAILS': 'gpxmap-details',
    'HBOX': 'gpxmap-hbox',
    'SELECT': 'gpxmap-select',
    'TRACK': 'gpxmap-track',
    'VBOX': 'gpxmap-vbox',
    'VIEW': 'gpxmap-view',
};

/**
 * If this looks familiar that's because it's binary search. We find
 * 0 <= i <= array.length such that !pred(i - 1) && pred(i) under the
 * assumption !pred(-1) && pred(length).
 */
function search(array, pred) {
    let le = -1, ri = array.length;
    while (1 + le !== ri) {
        let mi = le + ((ri - le) >> 1);
        if (pred(array[mi])) {
            ri = mi;
        } else {
            le = mi;
        }
    }
    return ri;
}

function yearColour(idx) {
    return ['hsl(',
        170 + 45 * (idx >> 1), ',',
        100, '%,',
        27 * (1 + idx % 2), '%)'
    ].join('');
}

function walkPopup(date, walk) {
    var popup = document.createElement('div');
    var anchor = document.createElement('a');
    var idx = search(walk.dates, d => date <= d);

    if (walk.dates[idx] !== date) {
        console.log('walkPopup', date, walk);
        return;
    }

    popup.textContent =
            [ +date.substr(8, 2)
            , +date.substr(5, 2)
            , +date.substr(0, 4)
            ].join('/') + ' ';
    anchor.setAttribute('href', walk.link);
    anchor.textContent = walk.title;
    popup.appendChild(anchor);
    popup.appendChild(document.createTextNode(
        ' '+ +(walk.distances[idx] / 1000).toFixed(1) +'km'
    ));
    return popup;
}

function gpxmap(id, options) {
    // Create all configured tile layers.
    var tileLayers = options.tileLayers.map(function (layer) {
        return {
            'name': layer.name,
            'tileLayer': L.tileLayer(layer.url, layer.options),
        };
    });

    // Create layers control.
    var layersControl = L.control.layers(null, null, { 'hideSingleBase':true });
    tileLayers.forEach(function (layer) {
        layersControl.addBaseLayer(layer.tileLayer, layer.name);
    });

    // Create the DOM structure for our map.
    var domContainer = L.DomUtil.get(id);
    L.DomUtil.addClass(domContainer, CSS.VBOX);

    // Create map with an initial tile layer and layers control.
    var gpxmap = L.map(L.DomUtil.create('div', CSS.VIEW, domContainer)).
        addControl(L.control.scale()).
        addControl(layersControl).
        addLayer(tileLayers[0].tileLayer);

    var domDetails = L.DomUtil.create('div', CSS.DETAILS, domContainer);

    var hiddenYear = {};
    function trackStyle(hover) {
        return function (props) {
            var year = props.date.substr(0, 4);
            return hiddenYear[year] ? [] : {
                'className': CSS.TRACK,
                'color': yearColour(year - 2011),
                'opacity': hover ? 1 : .8,
                'weight': hover ? 4 : 2,
            };
        };
    }

    // This layer shows walking tracks.
    var walkLayer = L.vectorGrid.protobuf(
        options.url, {
            'pane': 'overlayPane',
            'getFeatureId': function (walk) { return walk.properties.date; },
            'vectorTileLayerStyles': { '': trackStyle(false) },
        }
    ).addTo(gpxmap);

    // This layer has invisible mouse-responsive tracks.
    var mouseLayer = L.vectorGrid.protobuf(
        options.url, {
            'pane': 'overlayPane',
            'getFeatureId': function (walk) { return walk.properties.date; },
            'vectorTileLayerStyles': { '': function (props) {
                return hiddenYear[props.date.substr(0, 4)] ? [] : {
                    'opacity': 0,
                    'weight': 20,
                };
            } },
            'interactive': true,
        }
    ).on('mouseover', function (evt) {
        walkLayer.setFeatureStyle(evt.layer.properties.date, trackStyle(true));
    }).on('mouseout', function (evt) {
        walkLayer.resetFeatureStyle(evt.layer.properties.date);
    }).addTo(gpxmap);

    UTIL.load(options.index).then(function (walks) {
        // Collect all years.
        var years = {};
        walks.flatMap(function (walk) { return walk.dates; }).
            forEach(function (date) { years[date.substr(0, 4)] = true; });

        // Build popup from matching index entry.
        mouseLayer.on('click', function (evt) {
            var date = evt.layer.properties.date;
            var idx = search(walks, walk => date < walk.dates[0]) - 1;
            var popup = walkPopup(date, walks[idx]);
            this.closePopup().unbindPopup();
            if (popup) {
                this.bindPopup(popup).openPopup(evt.latlng);
            }
        });

        // Set up a summary pane that reacts when years are toggled.
        var sumPane = Summary.summaryPane(walks, function () {
            var h3 = L.DomUtil.create('h3');

            h3.textContent = options.title;
            L.DomUtil.empty(domDetails);
            domDetails.appendChild(h3);
            domDetails.appendChild(this.render());
        });

        // Create one layer group per year and add to the map.
        Object.keys(years).forEach(function (year) {
            var lg = L.layerGroup().on('add', function () {
                if (hiddenYear[year]) {
                    hiddenYear[year] = false;
                    walkLayer.redraw();
                    mouseLayer.redraw();
                }
            }).on('remove', function () {
                if (!hiddenYear[year]) {
                    hiddenYear[year] = true;
                    walkLayer.redraw();
                    mouseLayer.redraw();
                }
            });
            gpxmap.addLayer(lg);
            layersControl.addOverlay(lg, year);
            sumPane.addLayer(lg, year);
        });

        // Adjust the map's viewport when all GPX tracks are loaded
        var bounds = L.latLngBounds(walks.flatMap(function (walk) {
            return walk.bboxes;
        }).flatMap(function (bbox) {
            var l = bbox.length >> 1;
            return L.GeoJSON.coordsToLatLngs([
                [ bbox[0], bbox[1] ], [ bbox[l], bbox[1 + l] ]
            ]);
        }));
        gpxmap.setMaxBounds(bounds.pad(.05)).fitBounds(bounds);
    });

    return gpxmap;
}

// Export public symbols
return {
    'CSS': CSS,
    'gpxmap': gpxmap,
};
}());
<|endoftext|>


<commit_before>(function(app) {
  'use strict';

  var helper = {};

  helper.inherits = function(ctor, superCtor) {
    ctor.super_ = superCtor;
    ctor.prototype = Object.create(superCtor.prototype, {
      constructor: {
        value: ctor,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });

    return ctor;
  };

  helper.identity = function(value) {
    return value;
  };

  helper.clone = function(obj) {
    var ret = {};
    for (var key in obj) {
      ret[key] = obj[key];
    }
    return ret;
  };

  helper.extend = function(obj) {
    for (var i = 1, len = arguments.length; i < len; i++) {
      var src = arguments[i];
      for (var key in src) {
        obj[key] = src[key];
      }
    }
    return obj;
  };

  helper.pick = function(obj, keys) {
    var ret = {};
    if (!obj)
      return ret;
    keys.forEach(function(key) {
      if (key in obj)
        ret[key] = obj[key];
    });
    return ret;
  };

  helper.dig = function() {
    var args = Array.prototype.slice.call(arguments);
    return args.reduce(function(prev, curr) {
      return (typeof prev === 'object') ? prev[curr] : null;
    });
  };

  helper.sortBy = function(array, key) {
    return array.slice().sort(function(a, b) {
      if (a[key] > b[key])
        return 1;
      else if (a[key] < b[key])
        return -1;
      return 0;
    });
  };

  helper.List = (function() {
    var List = function() {
      this.data = [];
    };

    List.prototype.add = function(item) {
      if (!this.contains(item))
        this.data.push(item);
    };

    List.prototype.remove = function(item) {
      var data = this.data;

      for (var i = data.length - 1; i >= 0; i--) {
        if (this.equal(data[i], item)) {
          data.splice(i, 1);
          break;
        }
      }
    };

    List.prototype.contains = function(item) {
      return this.data.some(function(dataItem) {
        return this.equal(dataItem, item);
      }.bind(this));
    };

    List.prototype.equal = function(a, b) {
      return a === b;
    };

    List.prototype.toArray = function() {
      return this.data.slice();
    };

    return List;
  })();

  if (typeof module !== 'undefined' && module.exports)
    module.exports = helper;
  else
    app.helper = helper;
})(this.app || (this.app = {}));
<commit_msg>Add a helper method to remove an item from an array<commit_after>(function(app) {
  'use strict';

  var helper = {};

  helper.inherits = function(ctor, superCtor) {
    ctor.super_ = superCtor;
    ctor.prototype = Object.create(superCtor.prototype, {
      constructor: {
        value: ctor,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });

    return ctor;
  };

  helper.identity = function(value) {
    return value;
  };

  helper.clone = function(obj) {
    var ret = {};
    for (var key in obj) {
      ret[key] = obj[key];
    }
    return ret;
  };

  helper.extend = function(obj) {
    for (var i = 1, len = arguments.length; i < len; i++) {
      var src = arguments[i];
      for (var key in src) {
        obj[key] = src[key];
      }
    }
    return obj;
  };

  helper.pick = function(obj, keys) {
    var ret = {};
    if (!obj)
      return ret;
    keys.forEach(function(key) {
      if (key in obj)
        ret[key] = obj[key];
    });
    return ret;
  };

  helper.dig = function() {
    var args = Array.prototype.slice.call(arguments);
    return args.reduce(function(prev, curr) {
      return (typeof prev === 'object') ? prev[curr] : null;
    });
  };

  helper.sortBy = function(array, key) {
    return array.slice().sort(function(a, b) {
      if (a[key] > b[key])
        return 1;
      else if (a[key] < b[key])
        return -1;
      return 0;
    });
  };

  helper.remove = function(array, item) {
    array.splice(array.indexOf(item), 1);
  };

  helper.List = (function() {
    var List = function() {
      this.data = [];
    };

    List.prototype.add = function(item) {
      if (!this.contains(item))
        this.data.push(item);
    };

    List.prototype.remove = function(item) {
      var data = this.data;

      for (var i = data.length - 1; i >= 0; i--) {
        if (this.equal(data[i], item)) {
          data.splice(i, 1);
          break;
        }
      }
    };

    List.prototype.contains = function(item) {
      return this.data.some(function(dataItem) {
        return this.equal(dataItem, item);
      }.bind(this));
    };

    List.prototype.equal = function(a, b) {
      return a === b;
    };

    List.prototype.toArray = function() {
      return this.data.slice();
    };

    return List;
  })();

  if (typeof module !== 'undefined' && module.exports)
    module.exports = helper;
  else
    app.helper = helper;
})(this.app || (this.app = {}));
<|endoftext|>