var _ = require('underscore'); var Q = require('q'); var Backbone = require('backbone'); var LocaleStore = require('../stores/LocaleStore'); var util = require('../util'); var debounce = require('../util/debounce'); var intl = require('../intl'); var log = require('../log'); var KeyboardListener = require('../util/keyboard').KeyboardListener; var Main = require('../app'); var LevelStore = require('../stores/LevelStore'); var ModalTerminal = require('../views').ModalTerminal; var ContainedBase = require('../views').ContainedBase; var BaseView = require('../views').BaseView; var LEVELS = require('../../levels'); var LevelDropdownView = ContainedBase.extend({ tagName: 'div', className: 'levelDropdownView box vertical', template: _.template($('#level-dropdown-view').html()), events: { 'click div.levelDropdownTab': 'onTabClick' }, initialize: function(options) { options = options || {}; var queryParams = util.parseQueryString( window.location.href ); this.JSON = { selectedTab: queryParams.defaultTab || 'main', tabs: [{ id: 'main', name: intl.str('main-levels-tab') }, { id: 'remote', name: intl.str('remote-levels-tab') }] }; this.navEvents = Object.assign({}, Backbone.Events); this.navEvents.on('clickedID', debounce( this.loadLevelID.bind(this), 300, true )); this.navEvents.on('negative', this.negative, this); this.navEvents.on('positive', this.positive, this); this.navEvents.on('left', this.left, this); this.navEvents.on('right', this.right, this); this.navEvents.on('up', this.up, this); this.navEvents.on('down', this.down, this); this.keyboardListener = new KeyboardListener({ events: this.navEvents, aliasMap: { esc: 'negative', enter: 'positive' }, wait: true }); this.sequences = LevelStore.getSequences(); this.sequenceToLevels = LevelStore.getSequenceToLevels(); this.container = new ModalTerminal({ title: intl.str('select-a-level') }); // Lol WTF. For some reason we cant use this.render.bind(this) so // instead setup a lame callback version. The CasperJS tests // fail otherwise. var that = this; LocaleStore.subscribe(function() { that.render.apply(that); }); LevelStore.subscribe(function() { that.render(); }); this.render(); if (!options.wait) { this.show(); } }, render: function() { this.container.updateTitle( intl.str('select-a-level') ); this.updateTabNames([ intl.str('main-levels-tab'), intl.str('remote-levels-tab') ]); LevelDropdownView.__super__.render.apply(this, arguments); this.buildSequences(); }, onTabClick: function(ev) { var srcElement = ev.target || ev.srcElement; var id = $(srcElement).attr('data-id'); if (id === this.JSON.selectedTab) { return; } this.selectedTab = id; this.updateTabTo(id); }, updateTabTo: function(id) { this.JSON.selectedTab = id; this.render(); if (this.selectedID) { this.selectedSequence = this.getSequencesOnTab()[0]; this.selectedIndex = 0; this.updateSelectedIcon(); } }, updateTabNames: function(names) { for(var index = 0; index < names.length; ++index) { this.JSON.tabs[index].name = names[index]; } }, positive: function() { if (!this.selectedID) { return; } this.loadLevelID(this.selectedID); }, left: function() { if (this.turnOnKeyboardSelection()) { return; } this.leftOrRight(-1); }, updateSelectedIcon: function() { this.selectedID = this.getSelectedID(); this.selectIconByID(this.selectedID); }, leftOrRight: function(delta) { this.deselectIconByID(this.selectedID); var index = this.selectedIndex + delta; var sequence = this.getCurrentSequence(); var tabs = this.JSON.tabs; // switch tabs now if needed / possible if (index >= sequence.length && this.getTabIndex() + 1 < tabs.length) { this.switchToTabIndex(this.getTabIndex() + 1); this.selectedIndex = 0; } else if (index < 0 && this.getTabIndex() - 1 >= 0) { this.switchToTabIndex(this.getTabIndex() - 1); this.selectedIndex = 0; } else { this.selectedIndex = this.wrapIndex( this.selectedIndex + delta, this.getCurrentSequence() ); } this.updateSelectedIcon(); }, right: function() { if (this.turnOnKeyboardSelection()) { return; } this.leftOrRight(1); }, up: function() { if (this.turnOnKeyboardSelection()) { return; } this.selectedSequence = this.getPreviousSequence(); this.downOrUp(); }, down: function() { if (this.turnOnKeyboardSelection()) { return; } this.selectedSequence = this.getNextSequence(); this.downOrUp(); }, downOrUp: function() { this.selectedIndex = this.boundIndex(this.selectedIndex, this.getCurrentSequence()); this.deselectIconByID(this.selectedID); this.updateSelectedIcon(); }, turnOnKeyboardSelection: function() { if (!this.selectedID) { this.selectFirst(); return true; } return false; }, turnOffKeyboardSelection: function() { if (!this.selectedID) { return; } this.deselectIconByID(this.selectedID); this.selectedID = undefined; this.selectedIndex = undefined; this.selectedSequence = undefined; }, getTabIndex: function() { var ids = this.JSON.tabs.map(function(tab) { return tab.id; }); return ids.indexOf(this.JSON.selectedTab); }, switchToTabIndex: function(index) { var tabID = this.JSON.tabs[index].id; this.updateTabTo(tabID); }, wrapIndex: function(index, arr) { index = (index >= arr.length) ? 0 : index; index = (index < 0) ? arr.length - 1 : index; return index; }, boundIndex: function(index, arr) { index = (index >= arr.length) ? arr.length - 1 : index; index = (index < 0) ? 0 : index; return index; }, getSequencesOnTab: function() { return this.sequences.filter(function(sequenceName) { var tab = LEVELS.getTabForSequence(sequenceName); return tab === this.JSON.selectedTab; }, this); }, getNextSequence: function() { var current = this.getSequenceIndex(this.selectedSequence); var desired = this.wrapIndex(current + 1, this.getSequencesOnTab()); return this.getSequencesOnTab()[desired]; }, getPreviousSequence: function() { var current = this.getSequenceIndex(this.selectedSequence); var desired = this.wrapIndex(current - 1, this.getSequencesOnTab()); return this.getSequencesOnTab()[desired]; }, getSequenceIndex: function(name) { var index = this.getSequencesOnTab().indexOf(name); if (index < 0) { throw new Error('didnt find'); } return index; }, getIndexForID: function(id) { return LevelStore.getLevel(id).index; }, selectFirst: function() { var firstID = this.sequenceToLevels[this.getSequencesOnTab()[0]][0].id; this.selectIconByID(firstID); this.selectedIndex = 0; this.selectedSequence = this.getSequencesOnTab()[0]; }, getCurrentSequence: function() { return this.sequenceToLevels[this.selectedSequence]; }, getSelectedID: function() { return this.sequenceToLevels[this.selectedSequence][this.selectedIndex].id; }, selectIconByID: function(id) { this.toggleIconSelect(id, true); }, deselectIconByID: function(id) { this.toggleIconSelect(id, false); }, toggleIconSelect: function(id, value) { this.selectedID = id; var selector = '#levelIcon-' + id; $(selector).toggleClass('selected', value); // also go find the series and update the about this.seriesViews.forEach(function(view) { if (view.levelIDs.indexOf(id) === -1) { return; } view.updateAboutForLevelID(id); }, this); }, negative: function() { this.hide(); }, testOption: function(str) { return this.currentCommand && new RegExp('--' + str).test(this.currentCommand.get('rawStr')); }, show: function(deferred, command) { this.currentCommand = command; // doing the update on show will allow us to fade which will be nice this.updateSolvedStatus(); this.showDeferred = deferred; this.keyboardListener.listen(); LevelDropdownView.__super__.show.apply(this); }, hide: function() { if (this.showDeferred) { this.showDeferred.resolve(); } this.showDeferred = undefined; this.keyboardListener.mute(); this.turnOffKeyboardSelection(); LevelDropdownView.__super__.hide.apply(this); }, loadLevelID: function(id) { if (!this.testOption('noOutput')) { Main.getEventBaton().trigger( 'commandSubmitted', 'level ' + id ); var level = LevelStore.getLevel(id); var name = level.name.en_US; log.levelSelected(name); } this.hide(); }, updateSolvedStatus: function() { this.seriesViews.forEach(function(view) { view.updateSolvedStatus(); }, this); }, buildSequences: function() { this.seriesViews = []; this.getSequencesOnTab().forEach(function(sequenceName) { this.seriesViews.push(new SeriesView({ destination: this.$el, name: sequenceName, navEvents: this.navEvents })); }, this); } }); var SeriesView = BaseView.extend({ tagName: 'div', className: 'seriesView box flex1 vertical', template: _.template($('#series-view').html()), events: { 'click div.levelIcon': 'click', 'mouseenter div.levelIcon': 'enterIcon' }, initialize: function(options) { this.name = options.name || 'intro'; this.navEvents = options.navEvents; this.info = LevelStore.getSequenceInfo(this.name); this.levels = LevelStore.getLevelsInSequence(this.name); this.levelIDs = []; var firstLevelInfo = null; this.levels.forEach(function(level) { if (firstLevelInfo === null) { firstLevelInfo = this.formatLevelAbout(level.id); } this.levelIDs.push(level.id); }, this); this.destination = options.destination; // use a non-breaking space to prevent the level from bouncing around // from missing strings this.JSON = { displayName: intl.getIntlKey(this.info, 'displayName'), about: intl.getIntlKey(this.info, 'about') || " ", levelInfo: firstLevelInfo, ids: this.levelIDs }; this.render(); this.updateSolvedStatus(); }, updateSolvedStatus: function() { // this is a bit hacky, it really should be some nice model // property changing but it's the 11th hour... var toLoop = this.$('div.levelIcon').each(function(index, el) { var id = $(el).attr('data-id'); $(el).toggleClass('solved', LevelStore.isLevelSolved(id)); }); }, getEventID: function(ev) { var element = ev.target; return $(element).attr('data-id'); }, setAbout: function(content) { this.$('p.levelInfo').text(content); }, enterIcon: function(ev) { var id = this.getEventID(ev); this.updateAboutForLevelID(id); }, updateAboutForLevelID: function(id) { this.setAbout(this.formatLevelAbout(id)); }, formatLevelAbout: function(id) { var level = LevelStore.getLevel(id); return this.getLevelNumberFromID(id) + ': ' + intl.getName(level); }, getLevelNumberFromID: function(id) { // hack -- parse out the level number from the ID return id.replace(/[^0-9]/g, ''); }, click: function(ev) { var id = this.getEventID(ev); this.navEvents.trigger('clickedID', id); } }); exports.LevelDropdownView = LevelDropdownView;