pcottle.learnGitBranching/src/js/views/index.js
2014-05-08 02:38:46 -03:00

1013 lines
24 KiB
JavaScript

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;