var _ = require('underscore'); var Q = require('q'); var Backbone = require('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 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(function() { this.tearDown(); }.bind(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 || Object.assign({}, 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( this.sendClick.bind(this), 500 ); } this.clickFunc(); }, sendClick: function() { this.navEvents.trigger('click'); }.bind(this) }); 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()), 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 = Object.assign({}, Backbone.Events); this.JSON = { showLeft: (options.showLeft === undefined) ? true : options.showLeft, lastNav: (options.lastNav === undefined) ? false : options.lastNav }; this.render(); // For some weird reason backbone events aren't working anymore so // im going to just wire this up manually this.$('div.right').click(this.positive.bind(this)); this.$('div.left').click(this.negative.bind(this)); this.$('div.exit').click(this.exit.bind(this)); }, 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); } }); 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 doesn't 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(function() { this.toggleShow(true); }.bind(this)); }, hide: function() { this.toggleShow(false); setTimeout(function() { // if we are still hidden... if (!this.shown) { this.toggleZ(false); } }.bind(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 || Object.assign({}, Backbone.Events); this.container = new ModalView(); this.JSON = { title: options.title }; this.render(); }, updateTitle: function(/*string*/ title) { this.$('.modal-title').text(title); }, 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) { this.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({}); 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); // one more hack -- allow adding custom random HTML if specified if (this.options._dangerouslyInsertHTML) { HTML += this.options._dangerouslyInsertHTML; } // 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(Object.assign( {}, { 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(function() { this.close(); }.bind(this)); // also setup keyboard this.navEvents = Object.assign({}, 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'; var extraHTML; if (options.nextLevel) { markdown = markdown + intl.str('finish-dialog-next', {nextLevel: nextLevelName}); } else { extraHTML = '

' + intl.str('finish-dialog-finished') + ' (ノ^_^)ノ (ノ^_^)ノ (ノ^_^)ノ' + '

'; } options = Object.assign( {}, options, { markdown: markdown, _dangerouslyInsertHTML: extraHTML } ); NextLevelConfirm.__super__.initialize.apply(this, [options]); } }); 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 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(this.recalcLayout.bind(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(function() { this.tearDown(); }.bind(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.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.CanvasTerminalHolder = CanvasTerminalHolder; exports.NextLevelConfirm = NextLevelConfirm;