mirror of
https://github.com/pcottle/learnGitBranching.git
synced 2025-06-27 16:38:50 +02:00
views
This commit is contained in:
parent
4a9b21f2ce
commit
010ef3c6a3
5 changed files with 16 additions and 564 deletions
566
build/bundle.js
566
build/bundle.js
|
@ -396,7 +396,7 @@ require.define("/git.js",function(require,module,exports,__dirname,__filename,pr
|
|||
var animationFactory = new AnimationFactoryModule.AnimationFactory();
|
||||
var Main = require('./app/main');
|
||||
var AnimationQueue = require('./animation').AnimationQueue;
|
||||
var InteractiveRebaseView = require('./miscViews').InteractiveRebaseView;
|
||||
var InteractiveRebaseView = require('./views/miscViews').InteractiveRebaseView;
|
||||
|
||||
var Errors = require('./errors');
|
||||
var GitError = Errors.GitError;
|
||||
|
@ -2464,7 +2464,7 @@ $(document).ready(function(){
|
|||
|
||||
function UI() {
|
||||
var Collections = require('../collections');
|
||||
var CommandViews = require('../commandViews');
|
||||
var CommandViews = require('../views/commandViews');
|
||||
|
||||
this.commandCollection = new Collections.CommandCollection();
|
||||
|
||||
|
@ -4668,12 +4668,12 @@ exports.VisBranch = VisBranch;
|
|||
|
||||
});
|
||||
|
||||
require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection;
|
||||
var Main = require('./app/main');
|
||||
var Command = require('./models/commandModel').Command;
|
||||
var CommandEntry = require('./models/commandModel').CommandEntry;
|
||||
require.define("/views/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('../collections').CommandEntryCollection;
|
||||
var Main = require('../app/main');
|
||||
var Command = require('../models/commandModel').Command;
|
||||
var CommandEntry = require('../models/commandModel').CommandEntry;
|
||||
|
||||
var Errors = require('./errors');
|
||||
var Errors = require('../errors');
|
||||
var Warning = Errors.Warning;
|
||||
|
||||
var CommandPromptView = Backbone.View.extend({
|
||||
|
@ -5064,7 +5064,7 @@ exports.CommandLineHistoryView = CommandLineHistoryView;
|
|||
|
||||
});
|
||||
|
||||
require.define("/miscViews.js",function(require,module,exports,__dirname,__filename,process,global){var InteractiveRebaseView = Backbone.View.extend({
|
||||
require.define("/views/miscViews.js",function(require,module,exports,__dirname,__filename,process,global){var InteractiveRebaseView = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
template: _.template($('#interactive-rebase-template').html()),
|
||||
|
||||
|
@ -5413,403 +5413,6 @@ exports.CommandBuffer = CommandBuffer;
|
|||
});
|
||||
require("/collections.js");
|
||||
|
||||
require.define("/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var CommandEntryCollection = require('./collections').CommandEntryCollection;
|
||||
var Main = require('./app/main');
|
||||
var Command = require('./models/commandModel').Command;
|
||||
var CommandEntry = require('./models/commandModel').CommandEntry;
|
||||
|
||||
var Errors = require('./errors');
|
||||
var Warning = Errors.Warning;
|
||||
|
||||
var CommandPromptView = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
this.collection = options.collection;
|
||||
|
||||
// uses local storage
|
||||
this.commands = new CommandEntryCollection();
|
||||
this.commands.fetch({
|
||||
success: _.bind(function() {
|
||||
// reverse the commands. this is ugly but needs to be done...
|
||||
var commands = [];
|
||||
this.commands.each(function(c) {
|
||||
commands.push(c);
|
||||
});
|
||||
|
||||
commands.reverse();
|
||||
this.commands.reset();
|
||||
|
||||
_.each(commands, function(c) {
|
||||
this.commands.add(c);
|
||||
}, this);
|
||||
}, this)
|
||||
});
|
||||
|
||||
this.index = -1;
|
||||
|
||||
this.commandSpan = this.$('#prompt span.command')[0];
|
||||
this.commandCursor = this.$('#prompt span.cursor')[0];
|
||||
|
||||
// this is evil, but we will refer to HTML outside the document
|
||||
// and attach a click event listener so we can focus / unfocus
|
||||
$(document).delegate('#commandLineHistory', 'click', _.bind(function() {
|
||||
this.focus();
|
||||
}, this));
|
||||
|
||||
|
||||
$(document).delegate('#commandTextField', 'blur', _.bind(function() {
|
||||
this.blur();
|
||||
}, this));
|
||||
|
||||
Main.getEvents().on('processCommandFromEvent', this.addToCollection, this);
|
||||
Main.getEvents().on('submitCommandValueFromEvent', this.submitValue, this);
|
||||
Main.getEvents().on('rollupCommands', this.rollupCommands, this);
|
||||
|
||||
// hacky timeout focus
|
||||
setTimeout(_.bind(function() {
|
||||
this.focus();
|
||||
}, this), 100);
|
||||
},
|
||||
|
||||
events: {
|
||||
'keydown #commandTextField': 'onKey',
|
||||
'keyup #commandTextField': 'onKeyUp',
|
||||
'blur #commandTextField': 'hideCursor',
|
||||
'focus #commandTextField': 'showCursor'
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
$(this.commandCursor).toggleClass('shown', false);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
this.$('#commandTextField').focus();
|
||||
this.showCursor();
|
||||
},
|
||||
|
||||
hideCursor: function() {
|
||||
this.toggleCursor(false);
|
||||
},
|
||||
|
||||
showCursor: function() {
|
||||
this.toggleCursor(true);
|
||||
},
|
||||
|
||||
toggleCursor: function(state) {
|
||||
$(this.commandCursor).toggleClass('shown', state);
|
||||
},
|
||||
|
||||
onKey: function(e) {
|
||||
var el = e.srcElement;
|
||||
this.updatePrompt(el);
|
||||
},
|
||||
|
||||
onKeyUp: function(e) {
|
||||
this.onKey(e);
|
||||
|
||||
// we need to capture some of these events.
|
||||
// WARNING: this key map is not internationalized :(
|
||||
var keyMap = {
|
||||
// enter
|
||||
13: _.bind(function() {
|
||||
this.submit();
|
||||
}, this),
|
||||
// up
|
||||
38: _.bind(function() {
|
||||
this.commandSelectChange(1);
|
||||
}, this),
|
||||
// down
|
||||
40: _.bind(function() {
|
||||
this.commandSelectChange(-1);
|
||||
}, this)
|
||||
};
|
||||
|
||||
if (keyMap[e.which] !== undefined) {
|
||||
e.preventDefault();
|
||||
keyMap[e.which]();
|
||||
this.onKey(e);
|
||||
}
|
||||
},
|
||||
|
||||
badHtmlEncode: function(text) {
|
||||
return text.replace(/&/g,'&')
|
||||
.replace(/</g,'<')
|
||||
.replace(/</g,'<')
|
||||
.replace(/ /g,' ')
|
||||
.replace(/\n/g,'');
|
||||
},
|
||||
|
||||
updatePrompt: function(el) {
|
||||
// i WEEEPPPPPPpppppppppppp that this reflow takes so long. it adds this
|
||||
// super annoying delay to every keystroke... I have tried everything
|
||||
// to make this more performant. getting the srcElement from the event,
|
||||
// getting the value directly from the dom, etc etc. yet still,
|
||||
// there's a very annoying and sightly noticeable command delay.
|
||||
// try.github.com also has this, so I'm assuming those engineers gave up as
|
||||
// well...
|
||||
|
||||
var val = this.badHtmlEncode(el.value);
|
||||
this.commandSpan.innerHTML = val;
|
||||
|
||||
// now mutate the cursor...
|
||||
this.cursorUpdate(el.value.length, el.selectionStart, el.selectionEnd);
|
||||
// and scroll down due to some weird bug
|
||||
Main.getEvents().trigger('commandScrollDown');
|
||||
},
|
||||
|
||||
cursorUpdate: function(commandLength, selectionStart, selectionEnd) {
|
||||
// 10px for monospaced font...
|
||||
var widthPerChar = 10;
|
||||
|
||||
var numCharsSelected = Math.max(1, selectionEnd - selectionStart);
|
||||
var width = String(numCharsSelected * widthPerChar) + 'px';
|
||||
|
||||
// now for positioning
|
||||
var numLeft = Math.max(commandLength - selectionStart, 0);
|
||||
var left = String(-numLeft * widthPerChar) + 'px';
|
||||
// one reflow? :D
|
||||
$(this.commandCursor).css({
|
||||
width: width,
|
||||
left: left
|
||||
});
|
||||
},
|
||||
|
||||
commandSelectChange: function(delta) {
|
||||
this.index += delta;
|
||||
|
||||
// if we are over / under, display blank line. yes this eliminates your
|
||||
// partially edited command, but i doubt that is much in this demo
|
||||
if (this.index >= this.commands.length || this.index < 0) {
|
||||
this.clear();
|
||||
this.index = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// yay! we actually can display something
|
||||
var commandEntry = this.commands.toArray()[this.index].get('text');
|
||||
this.setTextField(commandEntry);
|
||||
},
|
||||
|
||||
clearLocalStorage: function() {
|
||||
this.commands.each(function(c) {
|
||||
Backbone.sync('delete', c, function() { });
|
||||
}, this);
|
||||
localStorage.setItem('CommandEntries', '');
|
||||
},
|
||||
|
||||
setTextField: function(value) {
|
||||
this.$('#commandTextField').val(value);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.setTextField('');
|
||||
},
|
||||
|
||||
submit: function() {
|
||||
var value = this.$('#commandTextField').val().replace('\n', '');
|
||||
this.clear();
|
||||
this.submitValue(value);
|
||||
},
|
||||
|
||||
rollupCommands: function(numBack) {
|
||||
var which = this.commands.toArray().slice(1, Number(numBack) + 1);
|
||||
which.reverse();
|
||||
|
||||
var str = '';
|
||||
_.each(which, function(commandEntry) {
|
||||
str += commandEntry.get('text') + ';';
|
||||
}, this);
|
||||
|
||||
console.log('the str', str);
|
||||
|
||||
var rolled = new CommandEntry({text: str});
|
||||
this.commands.unshift(rolled);
|
||||
Backbone.sync('create', rolled, function() { });
|
||||
},
|
||||
|
||||
submitValue: function(value) {
|
||||
// we should add if it's not a blank line and this is a new command...
|
||||
// or if we edited the command
|
||||
var shouldAdd = (value.length && this.index == -1) ||
|
||||
((value.length && this.index !== -1 &&
|
||||
this.commands.toArray()[this.index].get('text') !== value));
|
||||
|
||||
if (shouldAdd) {
|
||||
var commandEntry = new CommandEntry({text: value});
|
||||
this.commands.unshift(commandEntry);
|
||||
|
||||
// store to local storage
|
||||
Backbone.sync('create', commandEntry, function() { });
|
||||
|
||||
// if our length is too egregious, reset
|
||||
if (this.commands.length > 100) {
|
||||
this.clearLocalStorage();
|
||||
}
|
||||
}
|
||||
this.index = -1;
|
||||
|
||||
// split commands on semicolon
|
||||
_.each(value.split(';'), _.bind(function(command, index) {
|
||||
command = _.escape(command);
|
||||
|
||||
command = command
|
||||
.replace(/^(\s+)/, '')
|
||||
.replace(/(\s+)$/, '')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
|
||||
if (index > 0 && !command.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addToCollection(command);
|
||||
}, this));
|
||||
},
|
||||
|
||||
addToCollection: function(value) {
|
||||
var command = new Command({
|
||||
rawStr: value
|
||||
});
|
||||
this.collection.add(command);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// This is the view for all commands -- it will represent
|
||||
// their status (inqueue, processing, finished, error),
|
||||
// their value ("git commit --amend"),
|
||||
// and the result (either errors or warnings or whatever)
|
||||
var CommandView = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
model: Command,
|
||||
template: _.template($('#command-template').html()),
|
||||
|
||||
events: {
|
||||
'click': 'clicked'
|
||||
},
|
||||
|
||||
clicked: function(e) {
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.model.bind('change', this.wasChanged, this);
|
||||
this.model.bind('destroy', this.remove, this);
|
||||
},
|
||||
|
||||
wasChanged: function(model, changeEvent) {
|
||||
// for changes that are just comestic, we actually only want to toggle classes
|
||||
// with jquery rather than brutally delete a html of HTML
|
||||
var changes = changeEvent.changes;
|
||||
var changeKeys = _.keys(changes);
|
||||
if (_.difference(changeKeys, ['status']) === 0) {
|
||||
this.updateStatus();
|
||||
} else if (_.difference(changeKeys, ['error']) === 0) {
|
||||
// the above will
|
||||
this.render();
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
updateStatus: function() {
|
||||
var statuses = ['inqueue', 'processing', 'finished'];
|
||||
var toggleMap = {};
|
||||
_.each(statuses, function(status) {
|
||||
toggleMap[status] = false;
|
||||
});
|
||||
toggleMap[this.model.get('status')] = true;
|
||||
|
||||
var query = this.$('p.commandLine');
|
||||
|
||||
_.each(toggleMap, function(value, key) {
|
||||
query.toggleClass(key, value);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var json = _.extend(
|
||||
{
|
||||
resultType: '',
|
||||
result: '',
|
||||
formattedWarnings: this.model.getFormattedWarnings()
|
||||
},
|
||||
this.model.toJSON()
|
||||
);
|
||||
|
||||
this.$el.html(this.template(json));
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
$(this.el).hide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var CommandLineHistoryView = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
this.collection = options.collection;
|
||||
|
||||
this.collection.on('add', this.addOne, this);
|
||||
this.collection.on('reset', this.addAll, this);
|
||||
this.collection.on('all', this.render, this);
|
||||
|
||||
this.collection.on('change', this.scrollDown, this);
|
||||
|
||||
Main.getEvents().on('issueWarning', this.addWarning, this);
|
||||
Main.getEvents().on('commandScrollDown', this.scrollDown, this);
|
||||
},
|
||||
|
||||
addWarning: function(msg) {
|
||||
var err = new Warning({
|
||||
msg: msg
|
||||
});
|
||||
|
||||
var command = new Command({
|
||||
error: err,
|
||||
rawStr: 'Warning:'
|
||||
});
|
||||
|
||||
this.collection.add(command);
|
||||
},
|
||||
|
||||
scrollDown: function() {
|
||||
// if commandDisplay is ever bigger than #terminal, we need to
|
||||
// add overflow-y to terminal and scroll down
|
||||
var cD = $('#commandDisplay')[0];
|
||||
var t = $('#terminal')[0];
|
||||
|
||||
if ($(t).hasClass('scrolling')) {
|
||||
t.scrollTop = t.scrollHeight;
|
||||
return;
|
||||
}
|
||||
if (cD.clientHeight > t.clientHeight) {
|
||||
$(t).css('overflow-y', 'scroll');
|
||||
$(t).css('overflow-x', 'hidden');
|
||||
$(t).addClass('scrolling');
|
||||
t.scrollTop = t.scrollHeight;
|
||||
}
|
||||
},
|
||||
|
||||
addOne: function(command) {
|
||||
var view = new CommandView({
|
||||
model: command
|
||||
});
|
||||
this.$('#commandDisplay').append(view.render().el);
|
||||
this.scrollDown();
|
||||
},
|
||||
|
||||
addAll: function() {
|
||||
this.collection.each(this.addOne);
|
||||
}
|
||||
});
|
||||
|
||||
exports.CommandPromptView = CommandPromptView;
|
||||
exports.CommandLineHistoryView = CommandLineHistoryView;
|
||||
|
||||
|
||||
});
|
||||
require("/commandViews.js");
|
||||
|
||||
require.define("/constants.js",function(require,module,exports,__dirname,__filename,process,global){/**
|
||||
* Constants....!!!
|
||||
*/
|
||||
|
@ -5936,7 +5539,7 @@ require.define("/git.js",function(require,module,exports,__dirname,__filename,pr
|
|||
var animationFactory = new AnimationFactoryModule.AnimationFactory();
|
||||
var Main = require('./app/main');
|
||||
var AnimationQueue = require('./animation').AnimationQueue;
|
||||
var InteractiveRebaseView = require('./miscViews').InteractiveRebaseView;
|
||||
var InteractiveRebaseView = require('./views/miscViews').InteractiveRebaseView;
|
||||
|
||||
var Errors = require('./errors');
|
||||
var GitError = Errors.GitError;
|
||||
|
@ -7676,157 +7279,6 @@ exports.LevelEngine = LevelEngine;
|
|||
});
|
||||
require("/levels.js");
|
||||
|
||||
require.define("/miscViews.js",function(require,module,exports,__dirname,__filename,process,global){var InteractiveRebaseView = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
template: _.template($('#interactive-rebase-template').html()),
|
||||
|
||||
events: {
|
||||
'click #confirmButton': 'confirmed'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.hasClicked = false;
|
||||
this.rebaseCallback = options.callback;
|
||||
|
||||
this.rebaseArray = options.toRebase;
|
||||
|
||||
this.rebaseEntries = new RebaseEntryCollection();
|
||||
this.rebaseMap = {};
|
||||
this.entryObjMap = {};
|
||||
|
||||
this.rebaseArray.reverse();
|
||||
// make basic models for each commit
|
||||
_.each(this.rebaseArray, function(commit) {
|
||||
var id = commit.get('id');
|
||||
this.rebaseMap[id] = commit;
|
||||
this.entryObjMap[id] = new RebaseEntry({
|
||||
id: id
|
||||
});
|
||||
this.rebaseEntries.add(this.entryObjMap[id]);
|
||||
}, this);
|
||||
|
||||
this.render();
|
||||
|
||||
// show the dialog holder
|
||||
this.show();
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.toggleVisibility(true);
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.toggleVisibility(false);
|
||||
},
|
||||
|
||||
toggleVisibility: function(toggle) {
|
||||
console.log('toggling');
|
||||
$('#dialogHolder').toggleClass('shown', toggle);
|
||||
},
|
||||
|
||||
confirmed: function() {
|
||||
// we hide the dialog anyways, but they might be fast clickers
|
||||
if (this.hasClicked) {
|
||||
return;
|
||||
}
|
||||
this.hasClicked = true;
|
||||
|
||||
// first of all hide
|
||||
this.$el.css('display', 'none');
|
||||
|
||||
// get our ordering
|
||||
var uiOrder = [];
|
||||
this.$('ul#rebaseEntries li').each(function(i, obj) {
|
||||
uiOrder.push(obj.id);
|
||||
});
|
||||
|
||||
// now get the real array
|
||||
var toRebase = [];
|
||||
_.each(uiOrder, function(id) {
|
||||
// the model
|
||||
if (this.entryObjMap[id].get('pick')) {
|
||||
toRebase.unshift(this.rebaseMap[id]);
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.rebaseCallback(toRebase);
|
||||
|
||||
this.$el.html('');
|
||||
// garbage collection will get us
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var json = {
|
||||
num: this.rebaseArray.length
|
||||
};
|
||||
|
||||
this.$el.html(this.template(json));
|
||||
|
||||
// also render each entry
|
||||
var listHolder = this.$('ul#rebaseEntries');
|
||||
this.rebaseEntries.each(function(entry) {
|
||||
new RebaseEntryView({
|
||||
el: listHolder,
|
||||
model: entry
|
||||
});
|
||||
}, this);
|
||||
|
||||
// then make it reorderable..
|
||||
listHolder.sortable({
|
||||
distance: 5,
|
||||
placeholder: 'ui-state-highlight'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var RebaseEntry = Backbone.Model.extend({
|
||||
defaults: {
|
||||
pick: true
|
||||
},
|
||||
|
||||
toggle: function() {
|
||||
this.set('pick', !this.get('pick'));
|
||||
}
|
||||
});
|
||||
|
||||
var RebaseEntryCollection = Backbone.Collection.extend({
|
||||
model: RebaseEntry
|
||||
});
|
||||
|
||||
var RebaseEntryView = Backbone.View.extend({
|
||||
tagName: 'li',
|
||||
template: _.template($('#interactive-rebase-entry-template').html()),
|
||||
|
||||
toggle: function() {
|
||||
this.model.toggle();
|
||||
|
||||
// toggle a class also
|
||||
this.listEntry.toggleClass('notPicked', !this.model.get('pick'));
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var json = this.model.toJSON();
|
||||
this.$el.append(this.template(this.model.toJSON()));
|
||||
|
||||
// hacky :( who would have known jquery barfs on ids with %'s and quotes
|
||||
this.listEntry = this.$el.children(':last');
|
||||
|
||||
this.listEntry.delegate('#toggleButton', 'click', _.bind(function() {
|
||||
this.toggle();
|
||||
}, this));
|
||||
}
|
||||
});
|
||||
|
||||
exports.InteractiveRebaseView = InteractiveRebaseView;
|
||||
|
||||
|
||||
});
|
||||
require("/miscViews.js");
|
||||
|
||||
require.define("/tree.js",function(require,module,exports,__dirname,__filename,process,global){var Main = require('./app/main');
|
||||
var GRAPHICS = require('./constants').GRAPHICS;
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ $(document).ready(function(){
|
|||
|
||||
function UI() {
|
||||
var Collections = require('../collections');
|
||||
var CommandViews = require('../commandViews');
|
||||
var CommandViews = require('../views/commandViews');
|
||||
|
||||
this.commandCollection = new Collections.CommandCollection();
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ var AnimationFactoryModule = require('./animation/animationFactory');
|
|||
var animationFactory = new AnimationFactoryModule.AnimationFactory();
|
||||
var Main = require('./app/main');
|
||||
var AnimationQueue = require('./animation').AnimationQueue;
|
||||
var InteractiveRebaseView = require('./miscViews').InteractiveRebaseView;
|
||||
var InteractiveRebaseView = require('./views/miscViews').InteractiveRebaseView;
|
||||
|
||||
var Errors = require('./errors');
|
||||
var GitError = Errors.GitError;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
var CommandEntryCollection = require('./collections').CommandEntryCollection;
|
||||
var Main = require('./app/main');
|
||||
var Command = require('./models/commandModel').Command;
|
||||
var CommandEntry = require('./models/commandModel').CommandEntry;
|
||||
var CommandEntryCollection = require('../collections').CommandEntryCollection;
|
||||
var Main = require('../app/main');
|
||||
var Command = require('../models/commandModel').Command;
|
||||
var CommandEntry = require('../models/commandModel').CommandEntry;
|
||||
|
||||
var Errors = require('./errors');
|
||||
var Errors = require('../errors');
|
||||
var Warning = Errors.Warning;
|
||||
|
||||
var CommandPromptView = Backbone.View.extend({
|
Loading…
Add table
Add a link
Reference in a new issue