<commit_before>
// Author: Sergio Castaño Arteaga
// Email: sergio.castano.arteaga@gmail.com

(function(){

    var debug = false;

    // ***************************************************************************
    // Socket.io events
    // ***************************************************************************

    var socket = io.connect(window.location.host);

    // Connection established
    socket.on('connected', function (data) {
        console.log(data);

        // Get users connected to mainroom
        socket.emit('getUsersInRoom', {'room':'MainRoom'});

        if (debug) {
            // Subscription to rooms
            socket.emit('subscribe', {'username':'sergio', 'rooms':['sampleroom']});

            // Send sample message to room
            socket.emit('newMessage', {'room':'sampleroom', 'msg':'Hellooooooo!'});

            // Auto-disconnect after 10 minutes
            setInterval(function() {
                socket.emit('unsubscribe', {'rooms':['sampleroom']});
                socket.disconnect();
            }, 600000);
        }
    });

    // Disconnected from server
    socket.on('disconnect', function (data) {
        var info = {'room':'MainRoom', 'username':'ServerBot', 'msg':'----- Lost connection to server -----'};
        addMessage(info);
    });

    // Reconnected to server
    socket.on('reconnect', function (data) {
        var info = {'room':'MainRoom', 'username':'ServerBot', 'msg':'----- Reconnected to server -----'};
        addMessage(info);
    });

    // Subscription to room confirmed
    socket.on('subscriptionConfirmed', function(data) {
        // Create room space in interface
        if (!roomExists(data.room)) {
            addRoomTab(data.room);
            addRoom(data.room);
        }

        // Close modal if opened
        $('#modal_joinroom').modal('hide');
    });

    // Unsubscription to room confirmed
    socket.on('unsubscriptionConfirmed', function(data) {
        // Remove room space in interface
        if (roomExists(data.room)) {
            removeRoomTab(data.room);
            removeRoom(data.room);
        }
    });

    // User joins room
    socket.on('userJoinsRoom', function(data) {
        console.log("userJoinsRoom: %s", JSON.stringify(data));
        // Log join in conversation
        addMessage(data);

        // Add user to connected users list
        addUser(data);
    });

    // User leaves room
    socket.on('userLeavesRoom', function(data) {
        console.log("userLeavesRoom: %s", JSON.stringify(data));
        // Log leave in conversation
        addMessage(data);

        // Remove user from connected users list
        removeUser(data);
    });

    // Message received
    socket.on('newMessage', function (data) {
        console.log("newMessage: %s", JSON.stringify(data));
        addMessage(data);

        // Scroll down room messages
        var room_messages = '#'+data.room+' #room_messages';
        $(room_messages).animate({
            scrollTop: $(room_messages).height()
        }, 300);
    });

    // Users in room received
    socket.on('usersInRoom', function(data) {
        console.log('usersInRoom: %s', JSON.stringify(data));
        _.each(data.users, function(user) {
            addUser(user);
        });
    });

    // User nickname updated
    socket.on('userNicknameUpdated', function(data) {
        console.log("userNicknameUpdated: %s", JSON.stringify(data));
        updateNickname(data);

        msg = '----- ' + data.oldUsername + ' is now ' + data.newUsername + ' -----';
        var info = {'room':data.room, 'username':'ServerBot', 'msg':msg};
        addMessage(info);
    });

    // ***************************************************************************
    // Templates and helpers
    // ***************************************************************************

    var templates = {};
    var getTemplate = function(path, callback) {
        var source;
        var template;

        // Check first if we've the template cached
        if (_.has(templates, path)) {
            if (callback) callback(templates[path]);
        // If not we get and compile it
        } else {
            $.ajax({
                url: path,
                success: function(data) {
                    source = data;
                    template = Handlebars.compile(source);
                    // Store compiled template in cache
                    templates[path] = template;
                    if (callback) callback(template);
                }
            });
        }
    }

    // Add room tab
    var addRoomTab = function(room) {
        getTemplate('js/templates/room_tab.handlebars', function(template) {
            $('#rooms_tabs').append(template({'room':room}));
        });
    };

    // Remove room tab
    var removeRoomTab = function(room) {
        var tab_id = "#"+room+"_tab";
        $(tab_id).remove();
    };

    // Add room
    var addRoom = function(room) {
        getTemplate('js/templates/room.handlebars', function(template) {
            $('#rooms').append(template({'room':room}));

            // Toogle to created room
            var newroomtab = '[href="#'+room+'"]';
            $(newroomtab).click();

            // Get users connected to room
            socket.emit('getUsersInRoom', {'room':room});
        });
    };

    // Remove room
    var removeRoom = function(room) {
        var room_id = "#"+room;
        $(room_id).remove();
    };

    // Add message to room
    var addMessage = function(msg) {
        getTemplate('js/templates/message.handlebars', function(template) {
            var room_messages = '#'+msg.room+' #room_messages';
            $(room_messages).append(template(msg));
        });
    };

    // Add user to connected users list
    var addUser = function(user) {
        getTemplate('js/templates/user.handlebars', function(template) {
            var room_users = '#'+user.room+' #room_users';
            // Add only if it doesn't exist in the room
            var user_badge = '#'+user.room+' #'+user.id;
            if (!($(user_badge).length)) {
                $(room_users).append(template(user));
            }
        });
    }

    // Remove user from connected users list
    var removeUser = function(user) {
        var user_badge = '#'+user.room+' #'+user.id;
        $(user_badge).remove();
    };

    // Check if room exists
    var roomExists = function(room) {
        var room_selector = '#'+room;
        if ($(room_selector).length) {
            return true;
        } else {
            return false;
        }
    };

    // Get current room
    var getCurrentRoom = function() {
        return $('li[id$="_tab"][class="active"]').text();
    };

    // Get message text from input field
    var getMessageText = function() {
        var text = $('#message_text').val();
        $('#message_text').val("");
        return text;
    };

    // Get room name from input field
    var getRoomName = function() {
        var name = $('#room_name').val().trim();
        $('#room_name').val("");
        return name;
    };

    // Get nickname from input field
    var getNickname = function() {
        var nickname = $('#nickname').val();
        $('#nickname').val("");
        return nickname;
    };

    // Update nickname in badges
    var updateNickname = function(data) {
        var badges = '#'+data.room+' #'+data.id;
        $(badges).text(data.newUsername);
    };

    // ***************************************************************************
    // Events
    // ***************************************************************************

    // Send new message
    $('#b_send_message').click(function(eventObject) {
        eventObject.preventDefault();
        if ($('#message_text').val() != "") {
            socket.emit('newMessage', {'room':getCurrentRoom(), 'msg':getMessageText()});
        }
    });

    // Join new room
    $('#b_join_room').click(function(eventObject) {
        var roomName = getRoomName();

        if (roomName) {
            eventObject.preventDefault();
            socket.emit('subscribe', {'rooms':[roomName]});

        // Added error class if empty room name
        } else {
            $('#room_name').addClass('error');
        }
    });

    // Leave current room
    $('#b_leave_room').click(function(eventObject) {
        eventObject.preventDefault();
        var currentRoom = getCurrentRoom();
        if (currentRoom != 'MainRoom') {
            socket.emit('unsubscribe', {'rooms':[getCurrentRoom()]});

            // Toogle to MainRoom
            $('[href="#MainRoom"]').click();
        } else {
            console.log('Cannot leave MainRoom, sorry');
        }
    });

    // Remove error style to hide modal
    $('#modal_joinroom').on('hidden.bs.modal', function (e) {
        if ($('#room_name').hasClass('error')) {
            $('#room_name').removeClass('error');
        }
    });

    // Set nickname
    $('#b_set_nickname').click(function(eventObject) {
        eventObject.preventDefault();
        socket.emit('setNickname', {'username':getNickname()});

        // Close modal if opened
        $('#modal_setnick').modal('hide');
    });

})();

<commit_msg>Fixed issue getting correct height of the active messages room<commit_after>
// Author: Sergio Castaño Arteaga
// Email: sergio.castano.arteaga@gmail.com

(function(){

    var debug = false;

    // ***************************************************************************
    // Socket.io events
    // ***************************************************************************

    var socket = io.connect(window.location.host);

    // Connection established
    socket.on('connected', function (data) {
        console.log(data);

        // Get users connected to mainroom
        socket.emit('getUsersInRoom', {'room':'MainRoom'});

        if (debug) {
            // Subscription to rooms
            socket.emit('subscribe', {'username':'sergio', 'rooms':['sampleroom']});

            // Send sample message to room
            socket.emit('newMessage', {'room':'sampleroom', 'msg':'Hellooooooo!'});

            // Auto-disconnect after 10 minutes
            setInterval(function() {
                socket.emit('unsubscribe', {'rooms':['sampleroom']});
                socket.disconnect();
            }, 600000);
        }
    });

    // Disconnected from server
    socket.on('disconnect', function (data) {
        var info = {'room':'MainRoom', 'username':'ServerBot', 'msg':'----- Lost connection to server -----'};
        addMessage(info);
    });

    // Reconnected to server
    socket.on('reconnect', function (data) {
        var info = {'room':'MainRoom', 'username':'ServerBot', 'msg':'----- Reconnected to server -----'};
        addMessage(info);
    });

    // Subscription to room confirmed
    socket.on('subscriptionConfirmed', function(data) {
        // Create room space in interface
        if (!roomExists(data.room)) {
            addRoomTab(data.room);
            addRoom(data.room);
        }

        // Close modal if opened
        $('#modal_joinroom').modal('hide');
    });

    // Unsubscription to room confirmed
    socket.on('unsubscriptionConfirmed', function(data) {
        // Remove room space in interface
        if (roomExists(data.room)) {
            removeRoomTab(data.room);
            removeRoom(data.room);
        }
    });

    // User joins room
    socket.on('userJoinsRoom', function(data) {
        console.log("userJoinsRoom: %s", JSON.stringify(data));
        // Log join in conversation
        addMessage(data);

        // Add user to connected users list
        addUser(data);
    });

    // User leaves room
    socket.on('userLeavesRoom', function(data) {
        console.log("userLeavesRoom: %s", JSON.stringify(data));
        // Log leave in conversation
        addMessage(data);

        // Remove user from connected users list
        removeUser(data);
    });

    // Message received
    socket.on('newMessage', function (data) {
        console.log("newMessage: %s", JSON.stringify(data));
        addMessage(data);

        // Scroll down room messages
        var room_messages = '#'+data.room+' #room_messages';
        $(room_messages).animate({
            scrollTop: $(room_messages).prop('scrollHeight')
        }, 300);
    });

    // Users in room received
    socket.on('usersInRoom', function(data) {
        console.log('usersInRoom: %s', JSON.stringify(data));
        _.each(data.users, function(user) {
            addUser(user);
        });
    });

    // User nickname updated
    socket.on('userNicknameUpdated', function(data) {
        console.log("userNicknameUpdated: %s", JSON.stringify(data));
        updateNickname(data);

        msg = '----- ' + data.oldUsername + ' is now ' + data.newUsername + ' -----';
        var info = {'room':data.room, 'username':'ServerBot', 'msg':msg};
        addMessage(info);
    });

    // ***************************************************************************
    // Templates and helpers
    // ***************************************************************************

    var templates = {};
    var getTemplate = function(path, callback) {
        var source;
        var template;

        // Check first if we've the template cached
        if (_.has(templates, path)) {
            if (callback) callback(templates[path]);
        // If not we get and compile it
        } else {
            $.ajax({
                url: path,
                success: function(data) {
                    source = data;
                    template = Handlebars.compile(source);
                    // Store compiled template in cache
                    templates[path] = template;
                    if (callback) callback(template);
                }
            });
        }
    }

    // Add room tab
    var addRoomTab = function(room) {
        getTemplate('js/templates/room_tab.handlebars', function(template) {
            $('#rooms_tabs').append(template({'room':room}));
        });
    };

    // Remove room tab
    var removeRoomTab = function(room) {
        var tab_id = "#"+room+"_tab";
        $(tab_id).remove();
    };

    // Add room
    var addRoom = function(room) {
        getTemplate('js/templates/room.handlebars', function(template) {
            $('#rooms').append(template({'room':room}));

            // Toogle to created room
            var newroomtab = '[href="#'+room+'"]';
            $(newroomtab).click();

            // Get users connected to room
            socket.emit('getUsersInRoom', {'room':room});
        });
    };

    // Remove room
    var removeRoom = function(room) {
        var room_id = "#"+room;
        $(room_id).remove();
    };

    // Add message to room
    var addMessage = function(msg) {
        getTemplate('js/templates/message.handlebars', function(template) {
            var room_messages = '#'+msg.room+' #room_messages';
            $(room_messages).append(template(msg));
        });
    };

    // Add user to connected users list
    var addUser = function(user) {
        getTemplate('js/templates/user.handlebars', function(template) {
            var room_users = '#'+user.room+' #room_users';
            // Add only if it doesn't exist in the room
            var user_badge = '#'+user.room+' #'+user.id;
            if (!($(user_badge).length)) {
                $(room_users).append(template(user));
            }
        });
    }

    // Remove user from connected users list
    var removeUser = function(user) {
        var user_badge = '#'+user.room+' #'+user.id;
        $(user_badge).remove();
    };

    // Check if room exists
    var roomExists = function(room) {
        var room_selector = '#'+room;
        if ($(room_selector).length) {
            return true;
        } else {
            return false;
        }
    };

    // Get current room
    var getCurrentRoom = function() {
        return $('li[id$="_tab"][class="active"]').text();
    };

    // Get message text from input field
    var getMessageText = function() {
        var text = $('#message_text').val();
        $('#message_text').val("");
        return text;
    };

    // Get room name from input field
    var getRoomName = function() {
        var name = $('#room_name').val().trim();
        $('#room_name').val("");
        return name;
    };

    // Get nickname from input field
    var getNickname = function() {
        var nickname = $('#nickname').val();
        $('#nickname').val("");
        return nickname;
    };

    // Update nickname in badges
    var updateNickname = function(data) {
        var badges = '#'+data.room+' #'+data.id;
        $(badges).text(data.newUsername);
    };

    // ***************************************************************************
    // Events
    // ***************************************************************************

    // Send new message
    $('#b_send_message').click(function(eventObject) {
        eventObject.preventDefault();
        if ($('#message_text').val() != "") {
            socket.emit('newMessage', {'room':getCurrentRoom(), 'msg':getMessageText()});
        }
    });

    // Join new room
    $('#b_join_room').click(function(eventObject) {
        var roomName = getRoomName();

        if (roomName) {
            eventObject.preventDefault();
            socket.emit('subscribe', {'rooms':[roomName]});

        // Added error class if empty room name
        } else {
            $('#room_name').addClass('error');
        }
    });

    // Leave current room
    $('#b_leave_room').click(function(eventObject) {
        eventObject.preventDefault();
        var currentRoom = getCurrentRoom();
        if (currentRoom != 'MainRoom') {
            socket.emit('unsubscribe', {'rooms':[getCurrentRoom()]});

            // Toogle to MainRoom
            $('[href="#MainRoom"]').click();
        } else {
            console.log('Cannot leave MainRoom, sorry');
        }
    });

    // Remove error style to hide modal
    $('#modal_joinroom').on('hidden.bs.modal', function (e) {
        if ($('#room_name').hasClass('error')) {
            $('#room_name').removeClass('error');
        }
    });

    // Set nickname
    $('#b_set_nickname').click(function(eventObject) {
        eventObject.preventDefault();
        socket.emit('setNickname', {'username':getNickname()});

        // Close modal if opened
        $('#modal_setnick').modal('hide');
    });

})();

<|endoftext|>


<commit_before>require.config({
    paths: {
        jquery: 'libs/jquery-min',
        underscore: 'libs/underscore-min',
        backbone: 'libs/backbone-min',
        mustache: 'libs/mustache',
    },
    shim: {
        jquery: {
            exports: '$'
        },
        underscore: {
            exports: '_'
        },
        backbone: {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone'
        }
    },

    // add dummy params to break caching
    urlArgs: '__nocache__=' +  (new Date()).getTime()
});

require([
    'jquery',
    'underscore',
    'backbone',
    'mustache',
    'text!/static/templates/search_bar.html.mustache',
    'text!/static/templates/ac_suggestion.html.mustache'
],
function ($, _, Backbone, Mustache, tmplSearchBar, tmplAcSuggestion) {

// the search bar
var SearchBar = Backbone.Model.extend({
    defaults: {
        query: '',
        acSuggestionList: null,

        // minimum amount of time between AJAX calls
        minUpdateIntervalMs: 200,
        timeoutId: null // id for the last update timeout
    },

    updateQuery: function (query) {
        if (query !== this.get('query')) {
            this.set({'query': query});

            // clear any existing update timeout
            if (this.get('timeoutId') !== null) {
                clearTimeout(this.get('timeoutId'));
            }

            // set a timeout to update once input is done coming for a bit
            var timeoutId = setTimeout(_.bind(function () {
                // update suggestion list and clear the timeout id
                this.get('acSuggestionList').updateSuggestions(query);
                this.set({'timeoutId': null});

            }, this), this.get('minUpdateIntervalMs'));

            // store the timeout id for the next update call
            this.set({'timeoutId': timeoutId});
        }
    }
});

var SearchBarView = Backbone.View.extend({
    template: Mustache.compile(tmplSearchBar),

    events: {
        'keyup input': 'updateQuery',
        'keydown input': 'updateQuery'
    },

    defaults: {
        model: null
    },

    initialize: function () {
        // create the element and add it to the document
        this.setElement($(this.template({placeholder: 'search'})));
        this.$el.appendTo($('body'));

        // cache a ref to the input and focus it
        this.$input = this.$el.find('input');
        this.$input.focus();
    },

    updateQuery: function (e) {
        this.model.updateQuery(this.$input.val());
    }
});

// a single suggestion below the search bar
var AutocompleteSuggestion = Backbone.Model.extend({
    defaults: {
        provider: null,
        suggestion: ''
    }
});

// the suggestions collection below the search bar
var AutocompleteSuggestionList = Backbone.Collection.extend({
    model: AutocompleteSuggestion,
    url: '/search/autocomplete',

    updateSuggestions: function (query) {
        var xhr = $.getJSON(this.url, {'query': query});

        // update the collection on reset
        xhr.success(_.bind(function (data) {
            this.reset(data.results);
        }, this));
    }
});

var AutocompleteSuggestionListView = Backbone.View.extend({
    template: Mustache.compile(tmplAcSuggestion),

    defaults: {
        collection: null
    },

    initialize: function () {
        this.collection.on('reset', this.render, this);
    },

    render: function () {
        // get the suggestion list from the collection
        var suggestions = this.collection.toJSON();

        // clear out the old suggestions
        this.$el.children().remove();

        // add all the new suggestions
        _.each(suggestions, function (suggestion) {
            this.$el.append(this.template(suggestion));
        }, this);

        return this;
    },
});

//
// ENTRY POINT
//

$(function () {
    var searchBar = new SearchBar();
    var searchBarView = new SearchBarView({
        model: searchBar
    });

    var acSuggestionList = new AutocompleteSuggestionList();
    var acSuggestionListView = new AutocompleteSuggestionListView({
        collection: acSuggestionList,
        el: $('#search-bar ul'),
    });

    searchBar.set({acSuggestionList: acSuggestionList});
});

});
<commit_msg>Add suggestion click, use built-in query set event<commit_after>require.config({
    paths: {
        jquery: 'libs/jquery-min',
        underscore: 'libs/underscore-min',
        backbone: 'libs/backbone-min',
        mustache: 'libs/mustache',
    },
    shim: {
        jquery: {
            exports: '$'
        },
        underscore: {
            exports: '_'
        },
        backbone: {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone'
        }
    },

    // add dummy params to break caching
    urlArgs: '__nocache__=' +  (new Date()).getTime()
});

require([
    'jquery',
    'underscore',
    'backbone',
    'mustache',
    'text!/static/templates/search_bar.html.mustache',
    'text!/static/templates/ac_suggestion.html.mustache'
],
function ($, _, Backbone, Mustache, tmplSearchBar, tmplAcSuggestion) {

// the search bar
var SearchBar = Backbone.Model.extend({
    defaults: {
        query: '',
        acSuggestionList: null,

        // minimum amount of time between AJAX calls
        minUpdateIntervalMs: 200,
        timeoutId: null // id for the last update timeout
    },

    initialize: function () {
        this.on('change:query', this.updateSuggestions, this);
    },

    updateSuggestions: function () {
        // clear any existing update timeout
        if (this.get('timeoutId') !== null) {
            clearTimeout(this.get('timeoutId'));
        }

        // set a timeout to update once input is done coming for a bit
        var timeoutId = setTimeout(_.bind(function () {
            // update suggestion list and clear the timeout id
            this.get('acSuggestionList').updateSuggestions(this.get('query'));
            this.set({'timeoutId': null});

        }, this), this.get('minUpdateIntervalMs'));

        // store the timeout id for the next update call
        this.set({'timeoutId': timeoutId});
    }
});

var SearchBarView = Backbone.View.extend({
    template: Mustache.compile(tmplSearchBar),

    events: {
        'keyup input': 'updateQuery',
        'keydown input': 'updateQuery'
    },

    defaults: {
        model: null
    },

    initialize: function () {
        // create the element and add it to the document
        this.setElement($(this.template({placeholder: 'search'})));
        this.$el.appendTo($('body'));

        // cache a ref to the input and focus it
        this.$input = this.$el.find('input');
        this.$input.focus();

        // update the input if the query changes
        this.model.on('change:query', this.updateInput, this);
    },

    updateQuery: function () {
        this.model.set({query: this.$input.val()});
    },

    updateInput: function () {
        this.$input.val(this.model.get('query'));
    }
});

// a single suggestion below the search bar
var AutocompleteSuggestion = Backbone.Model.extend({
    defaults: {
        provider: null,
        suggestion: ''
    }
});

// the suggestions collection below the search bar
var AutocompleteSuggestionList = Backbone.Collection.extend({
    model: AutocompleteSuggestion,
    url: '/search/autocomplete',

    updateSuggestions: function (query) {
        var xhr = $.getJSON(this.url, {'query': query});

        // update the collection on reset
        xhr.success(_.bind(function (data) {
            this.reset(data.results);
        }, this));
    }
});

var AutocompleteSuggestionListView = Backbone.View.extend({
    template: Mustache.compile(tmplAcSuggestion),

    events: {
        'click li': 'clickSuggestion'
    },

    defaults: {
        collection: null
    },

    initialize: function (models, options) {
        this.collection.on('reset', this.render, this);
    },

    render: function () {
        // get the suggestion list from the collection
        var suggestions = this.collection.toJSON();

        // clear out the old suggestions
        this.$el.children().remove();

        // add all the new suggestions
        _.each(suggestions, function (suggestion) {
            this.$el.append(this.template(suggestion));
        }, this);

        return this;
    },

    clickSuggestion: function (e) {
        // set the query to the clicked value, then reset the suggestions
        this.model.set({query: $(e.target).text()});
        this.collection.reset();
    }
});

//
// ENTRY POINT
//

$(function () {
    var searchBar = new SearchBar();
    var searchBarView = new SearchBarView({
        model: searchBar
    });

    var acSuggestionList = new AutocompleteSuggestionList();
    var acSuggestionListView = new AutocompleteSuggestionListView({
        collection: acSuggestionList,
        model: searchBar,
        el: $('#search-bar ul')
    });

    searchBar.set({acSuggestionList: acSuggestionList});
});

});
<|endoftext|>


<commit_before>const choo = require('choo');
const html = require('choo/html');
const http = require('xhr');
const find = require('lodash/find');
const queryString = require('query-string');
const store = require('./utils/localstorage.js');
const scrollIntoView = require('scroll-into-view');

const app = choo();
const appURL = 'https://5calls.org';
const debug = false;
// const appURL = 'http://localhost:8090';

// get the stored zip location
cachedAddress = '';
store.getAll('org.5calls.location', (location) => {
  if (location.length > 0) {
   cachedAddress = location[0]
  }
});

// get the stored geo location
cachedGeo = '';
store.getAll('org.5calls.geolocation', (geo) => {
  if (geo.length > 0) {
    console.log("geo get",geo[0]);
    cachedGeo = geo[0]
  }
});

let cachedFetchingLocation = (cachedGeo === '') ? true : false;

// get the stored geo location
cachedAllowBrowserGeo = true;
store.getAll('org.5calls.allow_geolocation', (allowGeo) => {
  if (allowGeo.length > 0) {
    console.log("allowGeo get",allowGeo[0]);
    cachedAllowBrowserGeo = allowGeo[0]
  }
});

let cachedLocationFetchType = (cachedAllowBrowserGeo) ? 'browserGeolocation' : 'ipAddress';

// get the time the geo was last fetched
cachedGeoTime = '';
store.getAll('org.5calls.geolocation_time', (geo) => {
  if (geo.length > 0) {
    console.log("geo time get",geo[0]);
    cachedGeoTime = geo[0]
  }
});

cachedCity = '';
store.getAll('org.5calls.geolocation_city', (city) => {
  if (city.length > 0) {
    console.log("city get",city[0]);
    cachedCity = city[0]
  }
});

cachedFetchingLocation  = (cachedCity !== '') ? true : cachedFetchingLocation;
cachedLocationFetchType = (cachedAddress !== '') ? 'address' : cachedLocationFetchType;

// get the stored completed issues
completedIssues = [];
store.getAll('org.5calls.completed', (completed) => {
  completedIssues = completed == null ? [] : completed;
});

app.model({
  state: {
    // remote data
    issues: [],
    totalCalls: 0,
    splitDistrict: false,

    // manual input address
    address: cachedAddress,

    // automatically geolocating
    geolocation: cachedGeo,
    geoCacheTime: cachedGeoTime,
    allowBrowserGeo: cachedAllowBrowserGeo,
    cachedCity: cachedCity,

    // view state
    // getInfo: false,
    // activeIssue: false,
    // completeIssue: false,
    askingLocation: false,
    fetchingLocation: cachedFetchingLocation,
    locationFetchType: cachedLocationFetchType,
    contactIndex: 0,
    completedIssues: completedIssues,

    debug: debug,
  },

  reducers: {
    receiveIssues: (state, data) => {
      response = JSON.parse(data)
      issues = response.issues //.filter((v) => { return v.contacts.length > 0 });
      return { issues: issues, splitDistrict: response.splitDistrict }
    },
    receiveTotals: (state, data) => {
      totals = JSON.parse(data);
      return { totalCalls: totals.count }
    },
    receiveIPInfoLoc: (state, data) => {
      geo = response.loc
      city = response.city
      time = new Date().valueOf()
      store.replace("org.5calls.geolocation", 0, geo, () => {});
      store.replace("org.5calls.geolocation_city", 0, city, () => {});
      store.replace("org.5calls.geolocation_time", 0, time, () => {});
      return { geolocation: geo, cachedCity: city, geoCacheTime: time, fetchingLocation: false, askingLocation: false }
    },
    changeActiveIssue: (state, issueId) => {
      return { contactIndex: 0 }
    },
    setContactIndex: (state, data) => {
      if (data.newIndex != 0) {
        return { contactIndex: data.newIndex }
      } else {
        store.add("org.5calls.completed", data.issueid, () => {})
        return { contactIndex: 0, completedIssues: state.completedIssues.concat(data.issueid) }
      }
    },
    setAddress: (state, address) => {
      Raven.setExtraContext({ address: address })
      store.replace("org.5calls.location", 0, address, () => {});

      return { address: address, askingLocation: false }
    },
    setGeolocation: (state, data) => {
      store.replace("org.5calls.geolocation", 0, data, () => {});
      return { geolocation: data, fetchingLocation: false }
    },
    setCachedCity: (state, data) => {
      response = JSON.parse(data);
      if (response.normalizedLocation && state.cachedCity == '') {
        store.replace("org.5calls.geolocation_city", 0, response.normalizedLocation, () => {});
        return { cachedCity: response.normalizedLocation }
      } else {
        return null
      }
    },
    fetchingLocation: (state, data) => {
      return { fetchingLocation: data }
    },
    allowBrowserGeolocation: (state, data) => {
      store.replace("org.5calls.allow_geolocation", 0, data, () => {})
      return { allowBrowserGeo: data }
    },
    enterLocation: (state, data) => {
      scrollIntoView(document.querySelector('#address'));
      return { askingLocation: true }
    },
    setLocationFetchType: (state, data) => {
      let askingLocation = (data === 'address');
      return { locationFetchType: data, askingLocation: askingLocation, fetchingLocation: !askingLocation }
    },
    resetLocation: (state, data) => {
      store.remove("org.5calls.location", () => {});
      store.remove("org.5calls.geolocation", () => {});
      store.remove("org.5calls.geolocation_city", () => {});
      store.remove("org.5calls.geolocation_time", () => {});
      return { address: '', geolocation: '', cachedCity: '', geoCacheTime: '' }
    },
    resetCompletedIssues: (state, data) => {
      store.remove("org.5calls.completed", () => {});
      return { completedIssues: [] }
    },
    home: (state, data) => {
      return { activeIssue: false, getInfo: false }
    }
  },

  effects: {
    fetch: (state, data, send, done) => {
      address = "?address="
      if (state.address !== '') {
        address += state.address
      } else if (state.geolocation !== "") {
        address += state.geolocation
      }

      const issueURL = appURL+'/issues/'+address
      // console.log("fetching url",issueURL);
      http(issueURL, (err, res, body) => {
        send('setCachedCity', body, done)
        send('receiveIssues', body, done)
      })
    },
    getTotals: (state, data, send, done) => {
      http(appURL+'/report/', (err, res, body) => {
        send('receiveTotals', body, done)
      })
    },
    changeActiveIssueEffect: (state, issueId, send, done) => {
      send('location:set', "/#issue/"+issueId, done)
      send('changeActiveIssue', issueId, done)
    },
    setLocation: (state, data, send, done) => {
      send('setAddress', data, done);
      send('fetch', {}, done);
    },
    setBrowserGeolocation: (state, data, send, done) => {
      send('setGeolocation', data, done);
      send('fetch', {}, done);
    },
    unsetLocation: (state, data, send, done) => {
      send('resetLocation', data, done)
      send('startup', data, done)
    },
    fetchLocationBy: (state, data, send, done) => {
      send('setLocationFetchType', data, done)
      send('startup', data, done)
    },
    fetchLocationByIP: (state, data, send, done) => {
      http('https://ipinfo.io/json', (err, res, body) => {
        if (res.statusCode == 200) {
          try {
            response = JSON.parse(body)
            if (response.city != "") {
              send('receiveIPInfoLoc', response, done);
              send('fetch', {}, done);
            } else {
              send('fetchLocationBy', 'address', done);
              Raven.captureMessage("Location with no city: "+response.loc, { level: 'warning' });
            }
          } catch(e) {
            send('fetchLocationBy', 'address', done);
            Raven.setExtraContext({ json: data })
            Raven.captureMessage("Couldn't parse ipinfo json", { level: 'error' });
          }

        } else {
          send('fetchLocationBy', 'address', done);
          Raven.captureMessage("Non-200 from ipinfo", { level: 'info' });
        }
      })
    },
    fetchLocationByBrowswer: (state, data, send, done) => {
      let geoSuccess = function(position) {

        if (typeof position.coords !== 'undefined') {
          let lat = position.coords.latitude;
          let long = position.coords.longitude;

          if (lat && long) {
            let geo = Math.floor(lat*10000)/10000 + ',' + Math.floor(long*10000)/10000;
            send('allowBrowserGeolocation', true, done);
            send('setBrowserGeolocation', geo, done);
          } else {
            console.log("Error: bad browser location results");
            send('fetchLocationBy', 'ipAddress', done);
          }
        } else {
          console.log("Error: bad browser location results");
          send('fetchLocationBy', 'ipAddress', done);
        }
      }
      let geoError = function(error) {
        if (error.code === 1) {
          send('allowBrowserGeolocation', false, done);
        }
        send('fetchLocationBy', 'ipAddress', done);
        console.log("Error with browser location (code: " + error.code + ")");
      }

      navigator.geolocation.getCurrentPosition(geoSuccess, geoError);
    },
    startup: (state, data, send, done) => {
      // sometimes we trigger this again when reloading mainView, check for issues
      if (state.issues.length == 0 || state.geolocation == '') {
        // Check for browser support of geolocation
        if ((state.allowBrowserGeo !== false && navigator.geolocation) &&
          state.locationFetchType === 'browserGeolocation' && state.geolocation == '') {
          send('fetchLocationByBrowswer', {}, done);
        }
        else if (state.locationFetchType === 'ipAddress' && state.geolocation == '') {
          send('fetchLocationByIP', {}, done);
        }
        else if (state.address !== '' || state.geolocation !== '') {
          send('fetchingLocation', false, done);
          send('fetch', {}, done);
        }
      }
    },
    incrementContact: (state, data, send, done) => {
      const issue = find(state.issues, ['id', data.issueid]);

      if (state.contactIndex < issue.contacts.length - 1) {
        scrollIntoView(document.querySelector('#contact'));
        send('setContactIndex', { newIndex: state.contactIndex + 1, issueid: issue.id }, done)
      } else {
        scrollIntoView(document.querySelector('#content'));
        store.add("org.5calls.completed", issue.id, () => {})
        send('location:set', "/#done", done)
        send('setContactIndex', { newIndex: 0, issueid: issue.id }, done)
      }
    },
    callComplete: (state, data, send, done) => {
      ga('send', 'called', data.result);

      const body = queryString.stringify({ location: state.zip, result: data.result, contactid: data.contactid, issueid: data.issueid })
      http.post(appURL+'/report', { body: body, headers: {"Content-Type": "application/x-www-form-urlencoded"} }, (err, res, body) => {
        // don't really care about the result
      })
      send('incrementContact', data, done);
    },
    skipCall: (state, data, send, done) => {
      ga('send', 'called', 'skip');

      send('incrementContact', data, done);
    },
    activateIssue: (state, data, send, done) => {
      scrollIntoView(document.querySelector('#content'));
      location.hash = "issue/" + data.id;
    }
  },
});

app.router({ default: '/404' }, [
  ['/', require('./pages/mainView.js')],
  ['/issue', require('./pages/mainView.js'),
    [':issueid', require('./pages/mainView.js')]
  ],
  ['/about', require('./pages/aboutView.js')],
  ['/done', require('./pages/doneView.js')],
]);

const tree = app.start();
const rootNode = document.getElementById('root');
document.body.replaceChild(tree, rootNode);
<commit_msg>Fat arrows saved us, but the variable name was wrong.<commit_after>const choo = require('choo');
const html = require('choo/html');
const http = require('xhr');
const find = require('lodash/find');
const queryString = require('query-string');
const store = require('./utils/localstorage.js');
const scrollIntoView = require('scroll-into-view');

const app = choo();
const appURL = 'https://5calls.org';
const debug = false;
// const appURL = 'http://localhost:8090';

// get the stored zip location
cachedAddress = '';
store.getAll('org.5calls.location', (location) => {
  if (location.length > 0) {
   cachedAddress = location[0]
  }
});

// get the stored geo location
cachedGeo = '';
store.getAll('org.5calls.geolocation', (geo) => {
  if (geo.length > 0) {
    console.log("geo get",geo[0]);
    cachedGeo = geo[0]
  }
});

let cachedFetchingLocation = (cachedGeo === '') ? true : false;

// get the stored geo location
cachedAllowBrowserGeo = true;
store.getAll('org.5calls.allow_geolocation', (allowGeo) => {
  if (allowGeo.length > 0) {
    console.log("allowGeo get",allowGeo[0]);
    cachedAllowBrowserGeo = allowGeo[0]
  }
});

let cachedLocationFetchType = (cachedAllowBrowserGeo) ? 'browserGeolocation' : 'ipAddress';

// get the time the geo was last fetched
cachedGeoTime = '';
store.getAll('org.5calls.geolocation_time', (geo) => {
  if (geo.length > 0) {
    console.log("geo time get",geo[0]);
    cachedGeoTime = geo[0]
  }
});

cachedCity = '';
store.getAll('org.5calls.geolocation_city', (city) => {
  if (city.length > 0) {
    console.log("city get",city[0]);
    cachedCity = city[0]
  }
});

cachedFetchingLocation  = (cachedCity !== '') ? true : cachedFetchingLocation;
cachedLocationFetchType = (cachedAddress !== '') ? 'address' : cachedLocationFetchType;

// get the stored completed issues
completedIssues = [];
store.getAll('org.5calls.completed', (completed) => {
  completedIssues = completed == null ? [] : completed;
});

app.model({
  state: {
    // remote data
    issues: [],
    totalCalls: 0,
    splitDistrict: false,

    // manual input address
    address: cachedAddress,

    // automatically geolocating
    geolocation: cachedGeo,
    geoCacheTime: cachedGeoTime,
    allowBrowserGeo: cachedAllowBrowserGeo,
    cachedCity: cachedCity,

    // view state
    // getInfo: false,
    // activeIssue: false,
    // completeIssue: false,
    askingLocation: false,
    fetchingLocation: cachedFetchingLocation,
    locationFetchType: cachedLocationFetchType,
    contactIndex: 0,
    completedIssues: completedIssues,

    debug: debug,
  },

  reducers: {
    receiveIssues: (state, data) => {
      response = JSON.parse(data)
      issues = response.issues //.filter((v) => { return v.contacts.length > 0 });
      return { issues: issues, splitDistrict: response.splitDistrict }
    },
    receiveTotals: (state, data) => {
      totals = JSON.parse(data);
      return { totalCalls: totals.count }
    },
    receiveIPInfoLoc: (state, data) => {
      geo = data.loc
      city = data.city
      time = new Date().valueOf()
      store.replace("org.5calls.geolocation", 0, geo, () => {});
      store.replace("org.5calls.geolocation_city", 0, city, () => {});
      store.replace("org.5calls.geolocation_time", 0, time, () => {});
      return { geolocation: geo, cachedCity: city, geoCacheTime: time, fetchingLocation: false, askingLocation: false }
    },
    changeActiveIssue: (state, issueId) => {
      return { contactIndex: 0 }
    },
    setContactIndex: (state, data) => {
      if (data.newIndex != 0) {
        return { contactIndex: data.newIndex }
      } else {
        store.add("org.5calls.completed", data.issueid, () => {})
        return { contactIndex: 0, completedIssues: state.completedIssues.concat(data.issueid) }
      }
    },
    setAddress: (state, address) => {
      Raven.setExtraContext({ address: address })
      store.replace("org.5calls.location", 0, address, () => {});

      return { address: address, askingLocation: false }
    },
    setGeolocation: (state, data) => {
      store.replace("org.5calls.geolocation", 0, data, () => {});
      return { geolocation: data, fetchingLocation: false }
    },
    setCachedCity: (state, data) => {
      response = JSON.parse(data);
      if (response.normalizedLocation && state.cachedCity == '') {
        store.replace("org.5calls.geolocation_city", 0, response.normalizedLocation, () => {});
        return { cachedCity: response.normalizedLocation }
      } else {
        return null
      }
    },
    fetchingLocation: (state, data) => {
      return { fetchingLocation: data }
    },
    allowBrowserGeolocation: (state, data) => {
      store.replace("org.5calls.allow_geolocation", 0, data, () => {})
      return { allowBrowserGeo: data }
    },
    enterLocation: (state, data) => {
      scrollIntoView(document.querySelector('#address'));
      return { askingLocation: true }
    },
    setLocationFetchType: (state, data) => {
      let askingLocation = (data === 'address');
      return { locationFetchType: data, askingLocation: askingLocation, fetchingLocation: !askingLocation }
    },
    resetLocation: (state, data) => {
      store.remove("org.5calls.location", () => {});
      store.remove("org.5calls.geolocation", () => {});
      store.remove("org.5calls.geolocation_city", () => {});
      store.remove("org.5calls.geolocation_time", () => {});
      return { address: '', geolocation: '', cachedCity: '', geoCacheTime: '' }
    },
    resetCompletedIssues: (state, data) => {
      store.remove("org.5calls.completed", () => {});
      return { completedIssues: [] }
    },
    home: (state, data) => {
      return { activeIssue: false, getInfo: false }
    }
  },

  effects: {
    fetch: (state, data, send, done) => {
      address = "?address="
      if (state.address !== '') {
        address += state.address
      } else if (state.geolocation !== "") {
        address += state.geolocation
      }

      const issueURL = appURL+'/issues/'+address
      // console.log("fetching url",issueURL);
      http(issueURL, (err, res, body) => {
        send('setCachedCity', body, done)
        send('receiveIssues', body, done)
      })
    },
    getTotals: (state, data, send, done) => {
      http(appURL+'/report/', (err, res, body) => {
        send('receiveTotals', body, done)
      })
    },
    changeActiveIssueEffect: (state, issueId, send, done) => {
      send('location:set', "/#issue/"+issueId, done)
      send('changeActiveIssue', issueId, done)
    },
    setLocation: (state, data, send, done) => {
      send('setAddress', data, done);
      send('fetch', {}, done);
    },
    setBrowserGeolocation: (state, data, send, done) => {
      send('setGeolocation', data, done);
      send('fetch', {}, done);
    },
    unsetLocation: (state, data, send, done) => {
      send('resetLocation', data, done)
      send('startup', data, done)
    },
    fetchLocationBy: (state, data, send, done) => {
      send('setLocationFetchType', data, done)
      send('startup', data, done)
    },
    fetchLocationByIP: (state, data, send, done) => {
      http('https://ipinfo.io/json', (err, res, body) => {
        if (res.statusCode == 200) {
          try {
            response = JSON.parse(body)
            if (response.city != "") {
              send('receiveIPInfoLoc', response, done);
              send('fetch', {}, done);
            } else {
              send('fetchLocationBy', 'address', done);
              Raven.captureMessage("Location with no city: "+response.loc, { level: 'warning' });
            }
          } catch(e) {
            send('fetchLocationBy', 'address', done);
            Raven.setExtraContext({ json: data })
            Raven.captureMessage("Couldn't parse ipinfo json", { level: 'error' });
          }

        } else {
          send('fetchLocationBy', 'address', done);
          Raven.captureMessage("Non-200 from ipinfo", { level: 'info' });
        }
      })
    },
    fetchLocationByBrowswer: (state, data, send, done) => {
      let geoSuccess = function(position) {

        if (typeof position.coords !== 'undefined') {
          let lat = position.coords.latitude;
          let long = position.coords.longitude;

          if (lat && long) {
            let geo = Math.floor(lat*10000)/10000 + ',' + Math.floor(long*10000)/10000;
            send('allowBrowserGeolocation', true, done);
            send('setBrowserGeolocation', geo, done);
          } else {
            console.log("Error: bad browser location results");
            send('fetchLocationBy', 'ipAddress', done);
          }
        } else {
          console.log("Error: bad browser location results");
          send('fetchLocationBy', 'ipAddress', done);
        }
      }
      let geoError = function(error) {
        if (error.code === 1) {
          send('allowBrowserGeolocation', false, done);
        }
        send('fetchLocationBy', 'ipAddress', done);
        console.log("Error with browser location (code: " + error.code + ")");
      }

      navigator.geolocation.getCurrentPosition(geoSuccess, geoError);
    },
    startup: (state, data, send, done) => {
      // sometimes we trigger this again when reloading mainView, check for issues
      if (state.issues.length == 0 || state.geolocation == '') {
        // Check for browser support of geolocation
        if ((state.allowBrowserGeo !== false && navigator.geolocation) &&
          state.locationFetchType === 'browserGeolocation' && state.geolocation == '') {
          send('fetchLocationByBrowswer', {}, done);
        }
        else if (state.locationFetchType === 'ipAddress' && state.geolocation == '') {
          send('fetchLocationByIP', {}, done);
        }
        else if (state.address !== '' || state.geolocation !== '') {
          send('fetchingLocation', false, done);
          send('fetch', {}, done);
        }
      }
    },
    incrementContact: (state, data, send, done) => {
      const issue = find(state.issues, ['id', data.issueid]);

      if (state.contactIndex < issue.contacts.length - 1) {
        scrollIntoView(document.querySelector('#contact'));
        send('setContactIndex', { newIndex: state.contactIndex + 1, issueid: issue.id }, done)
      } else {
        scrollIntoView(document.querySelector('#content'));
        store.add("org.5calls.completed", issue.id, () => {})
        send('location:set', "/#done", done)
        send('setContactIndex', { newIndex: 0, issueid: issue.id }, done)
      }
    },
    callComplete: (state, data, send, done) => {
      ga('send', 'called', data.result);

      const body = queryString.stringify({ location: state.zip, result: data.result, contactid: data.contactid, issueid: data.issueid })
      http.post(appURL+'/report', { body: body, headers: {"Content-Type": "application/x-www-form-urlencoded"} }, (err, res, body) => {
        // don't really care about the result
      })
      send('incrementContact', data, done);
    },
    skipCall: (state, data, send, done) => {
      ga('send', 'called', 'skip');

      send('incrementContact', data, done);
    },
    activateIssue: (state, data, send, done) => {
      scrollIntoView(document.querySelector('#content'));
      location.hash = "issue/" + data.id;
    }
  },
});

app.router({ default: '/404' }, [
  ['/', require('./pages/mainView.js')],
  ['/issue', require('./pages/mainView.js'),
    [':issueid', require('./pages/mainView.js')]
  ],
  ['/about', require('./pages/aboutView.js')],
  ['/done', require('./pages/doneView.js')],
]);

const tree = app.start();
const rootNode = document.getElementById('root');
document.body.replaceChild(tree, rootNode);
<|endoftext|>


<commit_before>var main = (function($) {
    var _ = {
        isShow: true,
        /**
         * Settings.
         * @var {object}
         */
        settings: {
            // Preload all images.
            preload: false, // Slide duration (must match "duration.slide" in _vars.scss).
            slideDuration: 500, // Layout duration (must match "duration.layout" in _vars.scss).
            layoutDuration: 750, // Thumbnails per "row" (must match "misc.thumbnails-per-row" in _vars.scss).
            thumbnailsPerRow: 2, // Side of main wrapper (must match "misc.main-side" in _vars.scss).
            mainSide: 'right'
        },
        /**
         * Window.
         * @var {jQuery}
         */
        $window: null,
        /**
         * Body.
         * @var {jQuery}
         */
        $body: null,
        /**
         * Main wrapper.
         * @var {jQuery}
         */
        $main: null,
        /**
         * Thumbnails.
         * @var {jQuery}
         */
        $thumbnails: null,
        /**
         * Viewer.
         * @var {jQuery}
         */
        $viewer: null,
        /**
         * Toggle.
         * @var {jQuery}
         */
        $toggle: null,
        /**
         * Nav (next).
         * @var {jQuery}
         */
        $navNext: null,
        /**
         * Nav (previous).
         * @var {jQuery}
         */
        $navPrevious: null,
        /**
         * Slides.
         * @var {array}
         */
        slides: [],
        /**
         * Current slide index.
         * @var {integer}
         */
        current: null,
        /**
         * Lock state.
         * @var {bool}
         */
        locked: false,
        /**
         * Keyboard shortcuts.
         * @var {object}
         */
        keys: {
            // Escape: Toggle main wrapper.
            27: function() {
                _.toggle();
            }, // Up: Move up.
            38: function() {
                _.up();
            }, // Down: Move down.
            40: function() {
                _.down();
            }, // Space: Next.
            32: function() {
                _.next();
            }, // Right Arrow: Next.
            39: function() {
                _.next();
            }, // Left Arrow: Previous.
            37: function() {
                _.previous();
            }
        },
        /**
         * Initialize properties.
         */
        initProperties: function() {
            // Window, body.
            _.$window = $(window);
            _.$body = $('body');
            // Thumbnails.
            _.$thumbnails = $('#thumbnails');
            // Viewer.
            _.$viewer = $('
' + '
' + '

' + '

' + '

' + '
' + '
').appendTo(_.$body);
            // Nav.
            _.$navNext = _.$viewer.find('.nav-next');
            _.$navPrevious = _.$viewer.find('.nav-previous');
            // Main wrapper.
            _.$main = $('#main');
            // Toggle.
            $('

').appendTo(_.$main);
            _.$toggle = $('.toggle');
            // IE<9: Fix viewer width (no calc support).
            if (skel.vars.IEVersion < 9) _.$window.on('resize', function() {
                window.setTimeout(function() {
                    _.$viewer.css('width', _.$window.width() - _.$main.width());
                }, 100);
            }).trigger('resize');
        },
        /**
         * Initialize events.
         */
        initEvents: function() {
            // Window.
            // Remove is-loading-* classes on load.
            _.$window.on('load', function() {
                _.$body.removeClass('is-loading-0');
                window.setTimeout(function() {
                    _.$body.removeClass('is-loading-1');
                }, 100);
                window.setTimeout(function() {
                    _.$body.removeClass('is-loading-2');
                }, 100 + Math.max(_.settings.layoutDuration - 150, 0));
            });
            // Disable animations/transitions on resize.
            var resizeTimeout;
            _.$window.on('resize', function() {
                _.$body.addClass('is-loading-0');
                window.clearTimeout(resizeTimeout);
                resizeTimeout = window.setTimeout(function() {
                    _.$body.removeClass('is-loading-0');
                }, 100);
            });
            // Viewer.
            // Hide main wrapper on tap (<= medium only).
            _.$viewer.on('touchend', function() {
                if (skel.breakpoint('medium').active) _.hide();
                _.isShow = !_.isShow;
                var $child = _.$viewer.find('.inner,.slide .caption');
                !_.isShow && $child.fadeIn(350) || $child.fadeOut(350);
            });
            // Touch gestures.
            _.$viewer.on('touchstart', function(event) {
                // Record start position.
                _.$viewer.touchPosX = event.originalEvent.touches[0].pageX;
                _.$viewer.touchPosY = event.originalEvent.touches[0].pageY;
            }).on('touchmove', function(event) {
                // No start position recorded? Bail.
                if (_.$viewer.touchPosX === null || _.$viewer.touchPosY === null) return;
                // Calculate stuff.
                var diffX = _.$viewer.touchPosX - event.originalEvent.touches[0].pageX,
                    diffY = _.$viewer.touchPosY - event.originalEvent.touches[0].pageY;
                boundary = 20, delta = 50;
                // Swipe left (next).
                if ((diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta)) _.next();
                // Swipe right (previous).
                else if ((diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta))) _.previous();
                // Overscroll fix.
                var th = _.$viewer.outerHeight(),
                    ts = (_.$viewer.get(0).scrollHeight - _.$viewer.scrollTop());
                if ((_.$viewer.scrollTop() <= 0 && diffY < 0) || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
                    event.preventDefault();
                    event.stopPropagation();
                }
            });
            // Main.
            // Touch gestures.
            _.$main.on('touchstart', function(event) {
                // Bail on xsmall.
                if (skel.breakpoint('xsmall').active) return;
                // Record start position.
                _.$main.touchPosX = event.originalEvent.touches[0].pageX;
                _.$main.touchPosY = event.originalEvent.touches[0].pageY;
            }).on('touchmove', function(event) {
                // Bail on xsmall.
                if (skel.breakpoint('xsmall').active) return;
                // No start position recorded? Bail.
                if (_.$main.touchPosX === null || _.$main.touchPosY === null) return;
                // Calculate stuff.
                var diffX = _.$main.touchPosX - event.originalEvent.touches[0].pageX,
                    diffY = _.$main.touchPosY - event.originalEvent.touches[0].pageY;
                boundary = 20, delta = 50, result = false;
                // Swipe to close.
                switch (_.settings.mainSide) {
                    case 'left':
                        result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
                        break;
                    case 'right':
                        result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
                        break;
                    default:
                        break;
                }
                if (result) _.hide();
                // Overscroll fix.
                var th = _.$main.outerHeight(),
                    ts = (_.$main.get(0).scrollHeight - _.$main.scrollTop());
                if ((_.$main.scrollTop() <= 0 && diffY < 0) || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
                    event.preventDefault();
                    event.stopPropagation();
                }
            });
            // Toggle.
            _.$toggle.on('click', function() {
                _.toggle();
            });
            // Prevent event from bubbling up to "hide event on tap" event.
            _.$toggle.on('touchend', function(event) {
                event.stopPropagation();
            });
            // Nav.
            _.$navNext.on('click', function() {
                _.next();
            });
            _.$navPrevious.on('click', function() {
                _.previous();
            });
            // Keyboard shortcuts.
            // Ignore shortcuts within form elements.
            _.$body.on('keydown', 'input,select,textarea', function(event) {
                event.stopPropagation();
            });
            _.$window.on('keydown', function(event) {
                // Ignore if xsmall is active.
                if (skel.breakpoint('xsmall').active) return;
                // Check keycode.
                if (event.keyCode in _.keys) {
                    // Stop other events.
                    event.stopPropagation();
                    event.preventDefault();
                    // Call shortcut.
                    (_.keys[event.keyCode])();
                }
            });
        },
        /**
         * Initialize viewer.
         */
        initViewer: function() {
            // Bind thumbnail click event.
            _.$thumbnails.on('click', '.thumbnail', function(event) {
                var $this = $(this);
                // Stop other events.
                event.preventDefault();
                event.stopPropagation();
                // Locked? Blur.
                if (_.locked) $this.blur();
                // Switch to this thumbnail's slide.
                _.switchTo($this.data('index'));
            });
            // Create slides from thumbnails.
            _.$thumbnails.children().each(function() {
                var $this = $(this),
                    $thumbnail = $this.children('.thumbnail'),
                    s;
                // Slide object.
                s = {
                    $parent: $this,
                    $slide: null,
                    $slideImage: null,
                    $slideCaption: null,
                    url: $thumbnail.attr('href'),
                    loaded: false
                };
                // Parent.
                $this.attr('tabIndex', '-1');
                // Slide.
                // Create elements.
                s.$slide = $('

');
                // Image.
                s.$slideImage = s.$slide.children('.image');
                // Set background stuff.
                s.$slideImage.css('background-image', '').css('background-position', ($thumbnail.data('position') || 'center'));
                // Caption.
                s.$slideCaption = s.$slide.find('.caption');
                // Move everything *except* the thumbnail itself to the caption.
                $this.children().not($thumbnail).appendTo(s.$slideCaption);
                // Preload?
                if (_.settings.preload) {
                    // Force image to download.
                    var $img = $('');
                    // Set slide's background image to it.
                    s.$slideImage.css('background-image', 'url(' + s.url + ')');
                    // Mark slide as loaded.
                    s.$slide.addClass('loaded');
                    s.loaded = true;
                }
                // Add to slides array.
                _.slides.push(s);
                // Set thumbnail's index.
                $thumbnail.data('index', _.slides.length - 1);
            });
        },
        /**
         * Initialize stuff.
         */
        init: function() {
            // IE<10: Zero out transition delays.
            if (skel.vars.IEVersion < 10) {
                _.settings.slideDuration = 0;
                _.settings.layoutDuration = 0;
            }
            // Skel.
            skel.breakpoints({
                xlarge: '(max-width: 1680px)',
                large: '(max-width: 1280px)',
                medium: '(max-width: 980px)',
                small: '(max-width: 736px)',
                xsmall: '(max-width: 480px)'
            });
            // Everything else.
            _.initProperties();
            _.initViewer();
            _.initEvents();
            // Initial slide.
            window.setTimeout(function() {
                // Show first slide if xsmall isn't active or it just deactivated.
                skel.on('-xsmall !xsmall', function() {
                    if (_.current === null) _.switchTo(0, true);
                });
            }, 0);
        },
        /**
         * Switch to a specific slide.
         * @param {integer} index Index.
         */
        switchTo: function(index, noHide) {
            // Already at index and xsmall isn't active? Bail.
            if (_.current == index && !skel.breakpoint('xsmall').active) return;
            // Locked? Bail.
            if (_.locked) return;
            // Lock.
            _.locked = true;
            // Hide main wrapper if medium is active.
            if (!noHide && skel.breakpoint('medium').active && skel.vars.IEVersion > 8) _.hide();
            // Get slides.
            var oldSlide = (_.current !== null ? _.slides[_.current] : null),
                newSlide = _.slides[index];
            // Update current.
            _.current = index;
            // Deactivate old slide (if there is one).
            if (oldSlide) {
                // Thumbnail.
                oldSlide.$parent.removeClass('active');
                // Slide.
                oldSlide.$slide.removeClass('active');
            }
            // Activate new slide.
            // Thumbnail.
            newSlide.$parent.addClass('active').focus();
            // Slide.
            var f = function() {
                // Old slide exists? Detach it.
                if (oldSlide) oldSlide.$slide.detach();
                // Attach new slide.
                newSlide.$slide.appendTo(_.$viewer);
                // New slide not yet loaded?
                if (!newSlide.loaded) {
                    window.setTimeout(function() {
                        // Mark as loading.
                        newSlide.$slide.addClass('loading');
                        // Wait for it to load.
                        $('').on('load', function() {
                            //window.setTimeout(function() {
                            // Set background image.
                            newSlide.$slideImage.css('background-image', 'url(' + newSlide.url + ')');
                            // Mark as loaded.
                            newSlide.loaded = true;
                            newSlide.$slide.removeClass('loading');
                            // Mark as active.
                            newSlide.$slide.addClass('active');
                            // Unlock.
                            window.setTimeout(function() {
                                _.locked = false;
                            }, 100);
                            //}, 1000);
                        });
                    }, 100);
                }
                // Otherwise ...
                else {
                    window.setTimeout(function() {
                        // Mark as active.
                        newSlide.$slide.addClass('active');
                        // Unlock.
                        window.setTimeout(function() {
                            _.locked = false;
                        }, 100);
                    }, 100);
                }
            };
            // No old slide? Switch immediately.
            if (!oldSlide)
                (f)();
            // Otherwise, wait for old slide to disappear first.
            else window.setTimeout(f, _.settings.slideDuration);
        },
        /**
         * Switches to the next slide.
         */
        next: function() {
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length;
            if (c >= l - 1) i = 0;
            else i = c + 1;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Switches to the previous slide.
         */
        previous: function() {
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length;
            if (c <= 0) i = l - 1;
            else i = c - 1;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Switches to slide "above" current.
         */
        up: function() {
            // Fullscreen? Bail.
            if (_.$body.hasClass('fullscreen')) return;
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length,
                tpr = _.settings.thumbnailsPerRow;
            if (c <= (tpr - 1)) i = l - (tpr - 1 - c) - 1;
            else i = c - tpr;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Switches to slide "below" current.
         */
        down: function() {
            // Fullscreen? Bail.
            if (_.$body.hasClass('fullscreen')) return;
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length,
                tpr = _.settings.thumbnailsPerRow;
            if (c >= l - tpr) i = c - l + tpr;
            else i = c + tpr;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Shows the main wrapper.
         */
        show: function() {
            // Already visible? Bail.
            if (!_.$body.hasClass('fullscreen')) return;
            // Show main wrapper.
            _.$body.removeClass('fullscreen');
            // Focus.
            _.$main.focus();
        },
        /**
         * Hides the main wrapper.
         */
        hide: function() {
            // Already hidden? Bail.
            if (_.$body.hasClass('fullscreen')) return;
            // Hide main wrapper.
            _.$body.addClass('fullscreen');
            // Blur.
            _.$main.blur();
        },
        /**
         * Toggles main wrapper.
         */
        toggle: function() {
            if (_.$body.hasClass('fullscreen')) _.show();
            else _.hide();
        },
    };
    return _;
})(jQuery);
main.init();<commit_msg>fix some error<commit_after>var main = (function($) {
    var _ = {
        isShow: true,
        /**
         * Settings.
         * @var {object}
         */
        settings: {
            // Preload all images.
            preload: true, // Slide duration (must match "duration.slide" in _vars.scss).
            slideDuration: 500, // Layout duration (must match "duration.layout" in _vars.scss).
            layoutDuration: 750, // Thumbnails per "row" (must match "misc.thumbnails-per-row" in _vars.scss).
            thumbnailsPerRow: 2, // Side of main wrapper (must match "misc.main-side" in _vars.scss).
            mainSide: 'right'
        },
        /**
         * Window.
         * @var {jQuery}
         */
        $window: null,
        /**
         * Body.
         * @var {jQuery}
         */
        $body: null,
        /**
         * Main wrapper.
         * @var {jQuery}
         */
        $main: null,
        /**
         * Thumbnails.
         * @var {jQuery}
         */
        $thumbnails: null,
        /**
         * Viewer.
         * @var {jQuery}
         */
        $viewer: null,
        /**
         * Toggle.
         * @var {jQuery}
         */
        $toggle: null,
        /**
         * Nav (next).
         * @var {jQuery}
         */
        $navNext: null,
        /**
         * Nav (previous).
         * @var {jQuery}
         */
        $navPrevious: null,
        /**
         * Slides.
         * @var {array}
         */
        slides: [],
        /**
         * Current slide index.
         * @var {integer}
         */
        current: null,
        /**
         * Lock state.
         * @var {bool}
         */
        locked: false,
        /**
         * Keyboard shortcuts.
         * @var {object}
         */
        keys: {
            // Escape: Toggle main wrapper.
            27: function() {
                _.toggle();
            }, // Up: Move up.
            38: function() {
                _.up();
            }, // Down: Move down.
            40: function() {
                _.down();
            }, // Space: Next.
            32: function() {
                _.next();
            }, // Right Arrow: Next.
            39: function() {
                _.next();
            }, // Left Arrow: Previous.
            37: function() {
                _.previous();
            }
        },
        /**
         * Initialize properties.
         */
        initProperties: function() {
            // Window, body.
            _.$window = $(window);
            _.$body = $('body');
            // Thumbnails.
            _.$thumbnails = $('#thumbnails');
            // Viewer.
            _.$viewer = $('
' + '
' + '

' + '

' + '

' + '
' + '
').appendTo(_.$body);
            // Nav.
            _.$navNext = _.$viewer.find('.nav-next');
            _.$navPrevious = _.$viewer.find('.nav-previous');
            // Main wrapper.
            _.$main = $('#main');
            // Toggle.
            $('

').appendTo(_.$main);
            _.$toggle = $('.toggle');
            // IE<9: Fix viewer width (no calc support).
            if (skel.vars.IEVersion < 9) _.$window.on('resize', function() {
                window.setTimeout(function() {
                    _.$viewer.css('width', _.$window.width() - _.$main.width());
                }, 100);
            }).trigger('resize');
        },
        /**
         * Initialize events.
         */
        initEvents: function() {
            // Window.
            // Remove is-loading-* classes on load.
            _.$window.on('load', function() {
                _.$body.removeClass('is-loading-0');
                window.setTimeout(function() {
                    _.$body.removeClass('is-loading-1');
                }, 100);
                window.setTimeout(function() {
                    _.$body.removeClass('is-loading-2');
                }, 100 + Math.max(_.settings.layoutDuration - 150, 0));
            });
            // Disable animations/transitions on resize.
            var resizeTimeout;
            _.$window.on('resize', function() {
                _.$body.addClass('is-loading-0');
                window.clearTimeout(resizeTimeout);
                resizeTimeout = window.setTimeout(function() {
                    _.$body.removeClass('is-loading-0');
                }, 100);
            });
            // Viewer.
            // Hide main wrapper on tap (<= medium only).
            _.$viewer.on('touchend', function() {
                if (skel.breakpoint('medium').active) _.hide();
                _.isShow = !_.isShow;
                var $child = _.$viewer.find('.inner,.slide .caption');
                !_.isShow && $child.fadeIn(500) || $child.fadeOut(500);
            });
            // Touch gestures.
            _.$viewer.on('touchstart', function(event) {
                // Record start position.
                _.$viewer.touchPosX = event.originalEvent.touches[0].pageX;
                _.$viewer.touchPosY = event.originalEvent.touches[0].pageY;
            }).on('touchmove', function(event) {
                // No start position recorded? Bail.
                if (_.$viewer.touchPosX === null || _.$viewer.touchPosY === null) return;
                // Calculate stuff.
                var diffX = _.$viewer.touchPosX - event.originalEvent.touches[0].pageX,
                    diffY = _.$viewer.touchPosY - event.originalEvent.touches[0].pageY;
                boundary = 20, delta = 50;
                // Swipe left (next).
                if ((diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta)) _.next();
                // Swipe right (previous).
                else if ((diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta))) _.previous();
                // Overscroll fix.
                var th = _.$viewer.outerHeight(),
                    ts = (_.$viewer.get(0).scrollHeight - _.$viewer.scrollTop());
                if ((_.$viewer.scrollTop() <= 0 && diffY < 0) || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
                    event.preventDefault();
                    event.stopPropagation();
                }
            });
            // Main.
            // Touch gestures.
            _.$main.on('touchstart', function(event) {
                // Bail on xsmall.
                if (skel.breakpoint('xsmall').active) return;
                // Record start position.
                _.$main.touchPosX = event.originalEvent.touches[0].pageX;
                _.$main.touchPosY = event.originalEvent.touches[0].pageY;
            }).on('touchmove', function(event) {
                // Bail on xsmall.
                if (skel.breakpoint('xsmall').active) return;
                // No start position recorded? Bail.
                if (_.$main.touchPosX === null || _.$main.touchPosY === null) return;
                // Calculate stuff.
                var diffX = _.$main.touchPosX - event.originalEvent.touches[0].pageX,
                    diffY = _.$main.touchPosY - event.originalEvent.touches[0].pageY;
                boundary = 20, delta = 50, result = false;
                // Swipe to close.
                switch (_.settings.mainSide) {
                    case 'left':
                        result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
                        break;
                    case 'right':
                        result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
                        break;
                    default:
                        break;
                }
                if (result) _.hide();
                // Overscroll fix.
                var th = _.$main.outerHeight(),
                    ts = (_.$main.get(0).scrollHeight - _.$main.scrollTop());
                if ((_.$main.scrollTop() <= 0 && diffY < 0) || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
                    event.preventDefault();
                    event.stopPropagation();
                }
            });
            // Toggle.
            _.$toggle.on('click', function() {
                _.toggle();
            });
            // Prevent event from bubbling up to "hide event on tap" event.
            _.$toggle.on('touchend', function(event) {
                event.stopPropagation();
            });
            // Nav.
            _.$navNext.on('click', function() {
                _.next();
            });
            _.$navPrevious.on('click', function() {
                _.previous();
            });
            // Keyboard shortcuts.
            // Ignore shortcuts within form elements.
            _.$body.on('keydown', 'input,select,textarea', function(event) {
                event.stopPropagation();
            });
            _.$window.on('keydown', function(event) {
                // Ignore if xsmall is active.
                if (skel.breakpoint('xsmall').active) return;
                // Check keycode.
                if (event.keyCode in _.keys) {
                    // Stop other events.
                    event.stopPropagation();
                    event.preventDefault();
                    // Call shortcut.
                    (_.keys[event.keyCode])();
                }
            });
        },
        /**
         * Initialize viewer.
         */
        initViewer: function() {
            // Bind thumbnail click event.
            _.$thumbnails.on('click', '.thumbnail', function(event) {
                var $this = $(this);
                // Stop other events.
                event.preventDefault();
                event.stopPropagation();
                // Locked? Blur.
                if (_.locked) $this.blur();
                // Switch to this thumbnail's slide.
                _.switchTo($this.data('index'));
            });
            // Create slides from thumbnails.
            _.$thumbnails.children().each(function() {
                var $this = $(this),
                    $thumbnail = $this.children('.thumbnail'),
                    s;
                // Slide object.
                s = {
                    $parent: $this,
                    $slide: null,
                    $slideImage: null,
                    $slideCaption: null,
                    url: $thumbnail.attr('href'),
                    loaded: false
                };
                // Parent.
                $this.attr('tabIndex', '-1');
                // Slide.
                // Create elements.
                s.$slide = $('

');
                // Image.
                s.$slideImage = s.$slide.children('.image');
                // Set background stuff.
                s.$slideImage.css('background-image', '').css('background-position', ($thumbnail.data('position') || 'center'));
                // Caption.
                s.$slideCaption = s.$slide.find('.caption');
                // Move everything *except* the thumbnail itself to the caption.
                $this.children().not($thumbnail).appendTo(s.$slideCaption);
                // Preload?
                if (_.settings.preload) {
                    // Force image to download.
                    var $img = $('');
                    // Set slide's background image to it.
                    s.$slideImage.css('background-image', 'url(' + s.url + ')');
                    // Mark slide as loaded.
                    s.$slide.addClass('loaded');
                    s.loaded = true;
                }
                // Add to slides array.
                _.slides.push(s);
                // Set thumbnail's index.
                $thumbnail.data('index', _.slides.length - 1);
            });
        },
        /**
         * Initialize stuff.
         */
        init: function() {
            // IE<10: Zero out transition delays.
            if (skel.vars.IEVersion < 10) {
                _.settings.slideDuration = 0;
                _.settings.layoutDuration = 0;
            }
            // Skel.
            skel.breakpoints({
                xlarge: '(max-width: 1680px)',
                large: '(max-width: 1280px)',
                medium: '(max-width: 980px)',
                small: '(max-width: 736px)',
                xsmall: '(max-width: 480px)'
            });
            // Everything else.
            _.initProperties();
            _.initViewer();
            _.initEvents();
            // Initial slide.
            window.setTimeout(function() {
                // Show first slide if xsmall isn't active or it just deactivated.
                skel.on('-xsmall !xsmall', function() {
                    if (_.current === null) _.switchTo(0, true);
                });
            }, 0);
        },
        /**
         * Switch to a specific slide.
         * @param {integer} index Index.
         */
        switchTo: function(index, noHide) {
            // Already at index and xsmall isn't active? Bail.
            if (_.current == index && !skel.breakpoint('xsmall').active) return;
            // Locked? Bail.
            if (_.locked) return;
            // Lock.
            _.locked = true;
            // Hide main wrapper if medium is active.
            if (!noHide && skel.breakpoint('medium').active && skel.vars.IEVersion > 8) _.hide();
            // Get slides.
            var oldSlide = (_.current !== null ? _.slides[_.current] : null),
                newSlide = _.slides[index];
            // Update current.
            _.current = index;
            // Deactivate old slide (if there is one).
            if (oldSlide) {
                // Thumbnail.
                oldSlide.$parent.removeClass('active');
                // Slide.
                oldSlide.$slide.removeClass('active');
            }
            // Activate new slide.
            // Thumbnail.
            newSlide.$parent.addClass('active').focus();
            // Slide.
            var f = function() {
                // Old slide exists? Detach it.
                if (oldSlide) oldSlide.$slide.detach();
                // Attach new slide.
                newSlide.$slide.appendTo(_.$viewer);
                // New slide not yet loaded?
                if (!newSlide.loaded) {
                    window.setTimeout(function() {
                        // Mark as loading.
                        newSlide.$slide.addClass('loading');
                        // Wait for it to load.
                        $('').on('load', function() {
                            //window.setTimeout(function() {
                            // Set background image.
                            newSlide.$slideImage.css('background-image', 'url(' + newSlide.url + ')');
                            // Mark as loaded.
                            newSlide.loaded = true;
                            newSlide.$slide.removeClass('loading');
                            // Mark as active.
                            newSlide.$slide.addClass('active');
                            // Unlock.
                            window.setTimeout(function() {
                                _.locked = false;
                            }, 100);
                            //}, 1000);
                        });
                    }, 100);
                }
                // Otherwise ...
                else {
                    window.setTimeout(function() {
                        // Mark as active.
                        newSlide.$slide.addClass('active');
                        // Unlock.
                        window.setTimeout(function() {
                            _.locked = false;
                        }, 100);
                    }, 100);
                }
            };
            // No old slide? Switch immediately.
            if (!oldSlide)
                (f)();
            // Otherwise, wait for old slide to disappear first.
            else window.setTimeout(f, _.settings.slideDuration);
        },
        /**
         * Switches to the next slide.
         */
        next: function() {
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length;
            if (c >= l - 1) i = 0;
            else i = c + 1;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Switches to the previous slide.
         */
        previous: function() {
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length;
            if (c <= 0) i = l - 1;
            else i = c - 1;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Switches to slide "above" current.
         */
        up: function() {
            // Fullscreen? Bail.
            if (_.$body.hasClass('fullscreen')) return;
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length,
                tpr = _.settings.thumbnailsPerRow;
            if (c <= (tpr - 1)) i = l - (tpr - 1 - c) - 1;
            else i = c - tpr;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Switches to slide "below" current.
         */
        down: function() {
            // Fullscreen? Bail.
            if (_.$body.hasClass('fullscreen')) return;
            // Calculate new index.
            var i, c = _.current,
                l = _.slides.length,
                tpr = _.settings.thumbnailsPerRow;
            if (c >= l - tpr) i = c - l + tpr;
            else i = c + tpr;
            // Switch.
            _.switchTo(i);
        },
        /**
         * Shows the main wrapper.
         */
        show: function() {
            // Already visible? Bail.
            if (!_.$body.hasClass('fullscreen')) return;
            // Show main wrapper.
            _.$body.removeClass('fullscreen');
            // Focus.
            _.$main.focus();
        },
        /**
         * Hides the main wrapper.
         */
        hide: function() {
            // Already hidden? Bail.
            if (_.$body.hasClass('fullscreen')) return;
            // Hide main wrapper.
            _.$body.addClass('fullscreen');
            // Blur.
            _.$main.blur();
        },
        /**
         * Toggles main wrapper.
         */
        toggle: function() {
            if (_.$body.hasClass('fullscreen')) _.show();
            else _.hide();
        },
    };
    return _;
})(jQuery);
main.init();<|endoftext|>


<commit_before><commit_msg>fix return for hooks to allow cancel (#17)<commit_after><|endoftext|>