var _ = require('underscore'); var Q = require('q'); // horrible hack to get localStorage Backbone plugin var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone; var Main = require('../app'); var intl = require('../intl'); var log = require('../log'); var Constants = require('../util/constants'); var KeyboardListener = require('../util/keyboard').KeyboardListener; var GitError = require('../util/errors').GitError; var BaseView = Backbone.View.extend({ getDestination: function() { return this.destination || this.container.getInsideElement(); }, tearDown: function() { this.$el.remove(); if (this.container) { this.container.tearDown(); } }, renderAgain: function(HTML) { // flexibility HTML = HTML || this.template(this.JSON); this.$el.html(HTML); }, render: function(HTML) { this.renderAgain(HTML); var destination = this.getDestination(); $(destination).append(this.el); } }); var ResolveRejectBase = BaseView.extend({ resolve: function() { this.deferred.resolve(); }, reject: function() { this.deferred.reject(); } }); var PositiveNegativeBase = BaseView.extend({ positive: function() { this.navEvents.trigger('positive'); }, exit: function() { this.navEvents.trigger('exit'); }, negative: function() { this.navEvents.trigger('negative'); } }); var ContainedBase = BaseView.extend({ getAnimationTime: function() { return 700; }, show: function() { this.container.show(); }, hide: function() { this.container.hide(); }, die: function() { this.hide(); setTimeout(_.bind(function() { this.tearDown(); }, this), this.getAnimationTime() * 1.1); } }); var GeneralButton = ContainedBase.extend({ tagName: 'a', className: 'generalButton uiButton', template: _.template($('#general-button').html()), events: { 'click': 'click' }, initialize: function(options) { options = options || {}; this.navEvents = options.navEvents || _.clone(Backbone.Events); this.destination = options.destination; if (!this.destination) { this.container = new ModalTerminal(); } this.JSON = { buttonText: options.buttonText || 'General Button', wantsWrapper: (options.wantsWrapper !== undefined) ? options.wantsWrapper : true }; this.render(); if (this.container && !options.wait) { this.show(); } }, click: function() { if (!this.clickFunc) { this.clickFunc = _.throttle( _.bind(this.sendClick, this), 500 ); } this.clickFunc(); }, sendClick: function() { this.navEvents.trigger('click'); } }); var ConfirmCancelView = ResolveRejectBase.extend({ tagName: 'div', className: 'confirmCancelView box horizontal justify', template: _.template($('#confirm-cancel-template').html()), events: { 'click .confirmButton': 'resolve', 'click .cancelButton': 'reject' }, initialize: function(options) { if (!options.destination) { throw new Error('needmore'); } this.destination = options.destination; this.deferred = options.deferred || Q.defer(); this.JSON = { confirm: options.confirm || 'Confirm', cancel: options.cancel || 'Cancel' }; this.render(); } }); var LeftRightView = PositiveNegativeBase.extend({ tagName: 'div', className: 'leftRightView box horizontal center', template: _.template($('#left-right-template').html()), events: { 'click .left': 'negative', 'click .exit': 'exit', 'click .right': 'positive' }, exit: function() { this.pipeEvents.trigger('exit'); LeftRightView.__super__.exit.apply(this); }, positive: function() { this.pipeEvents.trigger('positive'); LeftRightView.__super__.positive.apply(this); }, negative: function() { this.pipeEvents.trigger('negative'); LeftRightView.__super__.negative.apply(this); }, initialize: function(options) { if (!options.destination || !options.events) { throw new Error('needmore'); } this.destination = options.destination; // we switch to a system where every leftrightview has its own // events system to add support for git demonstration view taking control of the // click events this.pipeEvents = options.events; this.navEvents = _.clone(Backbone.Events); this.JSON = { showLeft: (options.showLeft === undefined) ? true : options.showLeft, lastNav: (options.lastNav === undefined) ? false : options.lastNav }; this.render(); } }); var ModalView = Backbone.View.extend({ tagName: 'div', className: 'modalView box horizontal center transitionOpacityLinear', template: _.template($('#modal-view-template').html()), getAnimationTime: function() { return 700; }, initialize: function(options) { this.shown = false; this.render(); }, render: function() { // add ourselves to the DOM this.$el.html(this.template({})); $('body').append(this.el); // this doesnt necessarily show us though... }, stealKeyboard: function() { Main.getEventBaton().stealBaton('keydown', this.onKeyDown, this); Main.getEventBaton().stealBaton('keyup', this.onKeyUp, this); Main.getEventBaton().stealBaton('windowFocus', this.onWindowFocus, this); Main.getEventBaton().stealBaton('documentClick', this.onDocumentClick, this); // blur the text input field so keydown events will not be caught by our // preventDefaulters, allowing people to still refresh and launch inspector (etc) $('#commandTextField').blur(); }, releaseKeyboard: function() { Main.getEventBaton().releaseBaton('keydown', this.onKeyDown, this); Main.getEventBaton().releaseBaton('keyup', this.onKeyUp, this); Main.getEventBaton().releaseBaton('windowFocus', this.onWindowFocus, this); Main.getEventBaton().releaseBaton('documentClick', this.onDocumentClick, this); Main.getEventBaton().trigger('windowFocus'); }, onWindowFocus: function(e) { //console.log('window focus doing nothing', e); }, onDocumentClick: function(e) { //console.log('doc click doing nothing', e); }, onKeyDown: function(e) { e.preventDefault(); }, onKeyUp: function(e) { e.preventDefault(); }, show: function() { this.toggleZ(true); // on reflow, change our class to animate. for whatever // reason if this is done immediately, chrome might combine // the two changes and lose the ability to animate and it looks bad. process.nextTick(_.bind(function() { this.toggleShow(true); }, this)); }, hide: function() { this.toggleShow(false); setTimeout(_.bind(function() { // if we are still hidden... if (!this.shown) { this.toggleZ(false); } }, this), this.getAnimationTime()); }, getInsideElement: function() { return this.$('.contentHolder'); }, toggleShow: function(value) { // this prevents releasing keyboard twice if (this.shown === value) { return; } if (value) { this.stealKeyboard(); } else { this.releaseKeyboard(); } this.shown = value; this.$el.toggleClass('show', value); }, toggleZ: function(value) { this.$el.toggleClass('inFront', value); }, tearDown: function() { this.$el.html(''); $('body')[0].removeChild(this.el); } }); var ModalTerminal = ContainedBase.extend({ tagName: 'div', className: 'modalTerminal box flex1', template: _.template($('#terminal-window-template').html()), events: { 'click div.inside': 'onClick' }, initialize: function(options) { options = options || {}; this.navEvents = options.events || _.clone(Backbone.Events); this.container = new ModalView(); this.JSON = { title: options.title || 'Heed This Warning!' }; this.render(); }, onClick: function() { this.navEvents.trigger('click'); }, getInsideElement: function() { return this.$('.inside'); } }); var ModalAlert = ContainedBase.extend({ tagName: 'div', template: _.template($('#modal-alert-template').html()), initialize: function(options) { options = options || {}; this.JSON = { title: options.title || 'Something to say', text: options.text || 'Here is a paragraph', markdown: options.markdown }; if (options.markdowns) { this.JSON.markdown = options.markdowns.join('\n'); } this.container = new ModalTerminal({ title: 'Alert!' }); this.render(); if (!options.wait) { this.show(); } }, render: function() { var HTML = (this.JSON.markdown) ? require('markdown').markdown.toHTML(this.JSON.markdown) : this.template(this.JSON); // call to super, not super elegant but better than // copy paste code ModalAlert.__super__.render.apply(this, [HTML]); } }); var ConfirmCancelTerminal = Backbone.View.extend({ initialize: function(options) { options = options || {}; this.deferred = options.deferred || Q.defer(); this.modalAlert = new ModalAlert(_.extend( {}, { markdown: '#you sure?' }, options )); var buttonDefer = Q.defer(); this.buttonDefer = buttonDefer; this.confirmCancel = new ConfirmCancelView({ deferred: buttonDefer, destination: this.modalAlert.getDestination() }); // whenever they hit a button. make sure // we close and pass that to our deferred buttonDefer.promise .then(this.deferred.resolve) .fail(this.deferred.reject) .done(_.bind(function() { this.close(); }, this)); // also setup keyboard this.navEvents = _.clone(Backbone.Events); this.navEvents.on('positive', this.positive, this); this.navEvents.on('negative', this.negative, this); this.keyboardListener = new KeyboardListener({ events: this.navEvents, aliasMap: { enter: 'positive', esc: 'negative' } }); if (!options.wait) { this.modalAlert.show(); } }, positive: function() { this.buttonDefer.resolve(); }, negative: function() { this.buttonDefer.reject(); }, getAnimationTime: function() { return 700; }, show: function() { this.modalAlert.show(); }, hide: function() { this.modalAlert.hide(); }, getPromise: function() { return this.deferred.promise; }, close: function() { this.keyboardListener.mute(); this.modalAlert.die(); } }); var NextLevelConfirm = ConfirmCancelTerminal.extend({ initialize: function(options) { options = options || {}; var nextLevelName = (options.nextLevel) ? intl.getName(options.nextLevel) : ''; // lol hax var markdowns = intl.getDialog(require('../dialogs/nextLevel'))[0].options.markdowns; var markdown = markdowns.join('\n'); markdown = intl.template(markdown, { numCommands: options.numCommands, best: options.best }); if (options.numCommands <= options.best) { markdown = markdown + '\n\n' + intl.str('finish-dialog-win'); } else { markdown = markdown + '\n\n' + intl.str('finish-dialog-lose', {best: options.best}); } markdown = markdown + '\n\n'; if (options.nextLevel) { markdown = markdown + intl.str('finish-dialog-next', {nextLevel: nextLevelName}); } else { markdown = markdown + intl.str('finish-dialog-finished'); } options = _.extend( {}, options, { markdown: markdown } ); NextLevelConfirm.__super__.initialize.apply(this, [options]); } }); var BackgroundView = Backbone.View.extend({ initialize: function() { this.$body = $('body'); Main.getEvents().on('vcsModeChange', this.updateMode, this); }, updateMode: function(eventData) { eventData = eventData || {}; var isGit = eventData.mode === 'git'; this.$body.toggleClass('gitMode', isGit); this.$body.toggleClass('hgMode', !isGit); } }); var ViewportAlert = Backbone.View.extend({ initialize: function(options) { this.grabBatons(); this.modalAlert = new ModalAlert({ markdowns: this.markdowns }); this.modalAlert.show(); }, grabBatons: function() { Main.getEventBaton().stealBaton(this.eventBatonName, this.batonFired, this); }, releaseBatons: function() { Main.getEventBaton().releaseBaton(this.eventBatonName, this.batonFired, this); }, finish: function() { this.releaseBatons(); this.modalAlert.die(); } }); var WindowSizeAlertWindow = ViewportAlert.extend({ initialize: function(options) { this.eventBatonName = 'windowSizeCheck'; this.markdowns = [ '## That window size is not supported :-/', 'Please resize your window back to a supported size', '', '(and of course, pull requests to fix this are appreciated :D)' ]; WindowSizeAlertWindow.__super__.initialize.apply(this, [options]); }, batonFired: function(size) { if (size.w > Constants.VIEWPORT.minWidth && size.h > Constants.VIEWPORT.minHeight) { this.finish(); } } }); var ZoomAlertWindow = ViewportAlert.extend({ initialize: function(options) { if (!options || !options.level) { throw new Error('need level'); } this.eventBatonName = 'zoomChange'; this.markdowns = [ '## That zoom level of ' + options.level + ' is not supported :-/', 'Please zoom back to a supported zoom level with Ctrl + and Ctrl -', '', '(and of course, pull requests to fix this are appreciated :D)' ]; ZoomAlertWindow.__super__.initialize.apply(this, [options]); }, batonFired: function(level) { if (level <= Constants.VIEWPORT.maxZoom && level >= Constants.VIEWPORT.minZoom) { this.finish(); } } }); var LevelToolbar = BaseView.extend({ tagName: 'div', className: 'levelToolbarHolder', template: _.template($('#level-toolbar-template').html()), initialize: function(options) { options = options || {}; this.parent = options.parent; this.JSON = { name: options.name || 'Some level! (unknown name)' }; this.beforeDestination = $($('#commandLineHistory div.toolbar')[0]); this.render(); this.$goalButton = this.$el.find('#show-goal'); this.$objectiveButton = this.$el.find('#show-objective'); var parent = this.parent; this.$goalButton.on('click', function () { parent.trigger('toggleGoal'); }); this.$objectiveButton.on('click', function() { parent.trigger('toggleObjective'); }); if (!options.wait) { process.nextTick(_.bind(this.show, this)); } }, getAnimationTime: function() { return 700; }, render: function() { var HTML = this.template(this.JSON); this.$el.html(HTML); this.beforeDestination.after(this.el); }, die: function() { this.hide(); setTimeout(_.bind(function() { this.tearDown(); }, this), this.getAnimationTime()); }, hide: function() { this.$('div.toolbar').toggleClass('hidden', true); }, show: function() { this.$('div.toolbar').toggleClass('hidden', false); } }); var HelperBar = BaseView.extend({ getClassName: function() { return 'BaseHelperBar'; }, tagName: 'div', className: 'helperBar transitionAll', template: _.template($('#helper-bar-template').html()), events: { 'click a': 'onClick' }, onClick: function(ev) { var target = ev.target; var id = $(target).attr('data-id'); var funcName = 'on' + id[0].toUpperCase() + id.slice(1) + 'Click'; this[funcName].call(this); }, show: function() { this.$el.toggleClass('show', true); }, hide: function() { this.$el.toggleClass('show', false); if (this.deferred) { this.deferred.resolve(); } }, getItems: function() { return []; }, setupChildren: function() { }, fireCommand: function(command) { Main.getEventBaton().trigger('commandSubmitted', command); }, showDeferMe: function(otherBar) { this.hide(); var whenClosed = Q.defer(); otherBar.deferred = whenClosed; whenClosed.promise.then(_.bind(function() { this.show(); }, this)); otherBar.show(); }, onExitClick: function() { this.hide(); }, initialize: function(options) { options = options || {}; this.destination = $('body'); this.JSON = { items: this.getItems() }; this.render(); this.$el.addClass(this.getClassName()); this.setupChildren(); if (!options.wait) { this.show(); } } }); var IntlHelperBar = HelperBar.extend({ getClassName: function() { return 'IntlHelperBar'; }, getItems: function() { return [{ text: 'Git Branching', id: 'english' }, { text: '日本語版リポジトリ', id: 'japanese' }, { text: 'Git 브랜치 배우기', id: 'korean' }, { text: '学习 Git 分支', id: 'simpchinese' }, { text: '學習 Git 分支', id: 'tradchinese' }, { text: 'español', id: 'spanish' }, { text: 'français', id: 'french' }, { text: 'Deutsch', id: 'german' }, { icon: 'signout', id: 'exit' }]; }, fireCommand: function() { log.viewInteracted('intlSelect'); HelperBar.prototype.fireCommand.apply(this, arguments); }, onJapaneseClick: function() { this.fireCommand('locale ja; levels'); this.hide(); }, onEnglishClick: function() { this.fireCommand('locale en_US; levels'); this.hide(); }, onKoreanClick: function() { this.fireCommand('locale ko; levels'); this.hide(); }, onSpanishClick: function() { this.fireCommand('locale es_AR; levels'); this.hide(); }, onFrenchClick: function() { this.fireCommand('locale fr_FR; levels'); this.hide(); }, onGermanClick: function() { this.fireCommand('locale de_DE; levels'); this.hide(); }, onSimpchineseClick: function() { this.fireCommand('locale zh_CN; levels'); this.hide(); }, onTradchineseClick: function() { this.fireCommand('locale zh_TW; levels'); this.hide(); } }); var CommandsHelperBar = HelperBar.extend({ getClassName: function() { return 'CommandsHelperBar'; }, getItems: function() { return [{ text: 'Levels', id: 'levels' }, { text: 'Solution', id: 'solution' }, { text: 'Reset', id: 'reset' }, { text: 'Undo', id: 'undo' }, { text: 'Objective', id: 'objective' }, { text: 'Help', id: 'help' }, { icon: 'signout', id: 'exit' }]; }, fireCommand: function() { log.viewInteracted('helperBar'); HelperBar.prototype.fireCommand.apply(this, arguments); }, onSolutionClick: function() { this.fireCommand('show solution'); }, onObjectiveClick: function() { this.fireCommand('objective'); }, onLevelsClick: function() { this.fireCommand('levels'); }, onResetClick: function() { this.fireCommand('reset'); }, onUndoClick: function() { this.fireCommand('undo'); }, onHelpClick: function() { this.fireCommand('help general; git help'); } }); var MainHelperBar = HelperBar.extend({ getItems: function() { return [{ icon: 'question-sign', id: 'commands' }, { icon: 'globe', id: 'intl' }, { newPageLink: true, icon: 'facebook', id: 'fb', href: 'https://www.facebook.com/LearnGitBranching' }]; }, onFbClick: function() { log.viewInteracted('fbPageLink'); }, onIntlClick: function() { this.showDeferMe(this.intlHelper); log.viewInteracted('openIntlBar'); }, onCommandsClick: function() { this.showDeferMe(this.commandsHelper); log.viewInteracted('openCommandsBar'); }, setupChildren: function() { this.commandsHelper = new CommandsHelperBar({ wait: true }); this.intlHelper = new IntlHelperBar({ wait: true}); } }); var CanvasTerminalHolder = BaseView.extend({ tagName: 'div', className: 'canvasTerminalHolder box flex1', template: _.template($('#terminal-window-bare-template').html()), events: { 'click div.wrapper': 'onClick' }, initialize: function(options) { options = options || {}; this.parent = options.parent; this.minHeight = options.minHeight || 200; this.destination = $('body'); this.JSON = { title: options.title || intl.str('goal-to-reach'), text: options.text || intl.str('hide-goal') }; this.render(); this.inDom = true; this.$terminal = this.$el.find('.terminal-window-holder').first(); this.$terminal.height(0.8 * $(window).height()); this.$terminal.draggable({ cursor: 'move', handle: '.toolbar', containment: '#interfaceWrapper', scroll: false }); // If the entire window gets resized such that the terminal is outside the view, then // move it back into the view, and expand/shrink it vertically as necessary. $(window).on('resize', _.debounce(_.bind(this.recalcLayout, this), 300)); if (options.additionalClass) { this.$el.addClass(options.additionalClass); } }, getAnimationTime: function() { return 700; }, onClick: function() { this.die(); }, die: function() { this.minimize(); this.inDom = false; setTimeout(_.bind(function() { this.tearDown(); }, this), this.getAnimationTime()); }, minimize: function() { this.parent.trigger('minimizeCanvas', { left: this.$terminal.css('left'), top: this.$terminal.css('top') }, { width: this.$terminal.css('width'), height: this.$terminal.css('height') }); this.$terminal.animate({ height: '0px', opacity: 0 }, this.getAnimationTime()); }, restore: function (pos, size) { var self = this; pos = pos || { top: this.$terminal.css('top'), left: this.$terminal.css('left') }; size = size || { width: this.$terminal.css('width'), height: this.$terminal.css('height') }; this.$terminal.css({ top: pos.top, left: pos.left, width: size.width, height: '0px', opacity: '0' }); this.$terminal.animate({ height: size.height, opacity: 1 }, this.getAnimationTime(), function() { self.recalcLayout(); }); }, recalcLayout: function () { // Resize/reposition self based on the size of the browser window. var parent = this.parent, leftOffset = 0, topOffset = 0, heightOffset = 0, width = this.$terminal.outerWidth(), height = this.$terminal.outerHeight(), left = this.$terminal.offset().left, top = this.$terminal.offset().top, right = ($(window).width() - (left + width)), bottom = ($(window).height() - (top + height)), minHeight = 0.75 * $(window).height(), maxHeight = 0.95 * $(window).height(); // Calculate offsets if (top < 0) { topOffset = -top; } if (left < 0) { leftOffset = -left; } if (right < 0) { leftOffset = right; } if (bottom < 0) { topOffset = bottom; } if (height < minHeight) { heightOffset = minHeight - height; } if (height > maxHeight) { heightOffset = maxHeight - height; } // Establish limits left = Math.max(left + leftOffset, 0); top = Math.max(top + topOffset, 0); height = Math.max(height + heightOffset, minHeight); // Set the new position/size this.$terminal.animate({ left: left + 'px', top: top + 'px', height: height + 'px' }, this.getAnimationTime(), function () { parent.trigger('resizeCanvas'); }); }, getCanvasLocation: function() { return this.$('div.inside')[0]; } }); exports.BaseView = BaseView; exports.BackgroundView = BackgroundView; exports.GeneralButton = GeneralButton; exports.ModalView = ModalView; exports.ModalTerminal = ModalTerminal; exports.ModalAlert = ModalAlert; exports.ContainedBase = ContainedBase; exports.ConfirmCancelView = ConfirmCancelView; exports.LeftRightView = LeftRightView; exports.ZoomAlertWindow = ZoomAlertWindow; exports.ConfirmCancelTerminal = ConfirmCancelTerminal; exports.WindowSizeAlertWindow = WindowSizeAlertWindow; exports.MainHelperBar = MainHelperBar; exports.CanvasTerminalHolder = CanvasTerminalHolder; exports.LevelToolbar = LevelToolbar; exports.NextLevelConfirm = NextLevelConfirm;