// TimeStamper namespace var TS = {}; /* Setup after the document is ready for manipulation. */ $(document).ready(function(){ // ======== DEFINE MODELS ========// /* Entry model. * Attributes * - id * - mark * - notes * - start */ TS.EntryModel = Backbone.Model.extend({ }); /* Timeline model. * Attributes: * - id * - description * - created */ TS.TimelineModel = Backbone.Model.extend({ }); /* User model. * Attributes: * - username * - fullname * - email * - join_date */ TS.UserModel = Backbone.Model.extend({ url: function() { return '/ts_api/users/' + this.get('id'); }, initialize: function(attrs, options) { _.bind(this, 'url'); } }); TS.EntryList = Backbone.Collection.extend({ model: TS.EntryModel, comparator: function(entry) { return entry.get('timestamp'); }, initialize: function(model, options) { if (options.timeline == undefined) { throw "Cannot create an EntryList without a TimelineModel reference." } else { this.timeline = options.timeline; } _.bindAll(this, "url"); }, url: function() { return "/ts_api/entries/" + this.timeline.get('user_id') + "/" + this.timeline.get('id'); } }); TS.TimelineList = Backbone.Collection.extend({ model: TS.TimelineModel, initialize: function(models, options) { if (options.user == undefined) { throw "Cannot create a TimelineList without a UserModel reference."; } else { this.user = options.user; } _.bindAll(this, 'url'); }, comparator: function(timeline) { return timeline.get('id'); }, url: function() { return "/ts_api/timelines/" + this.user.get('id'); } }); // ======== DEFINE VIEWS ========// /* Entry view */ TS.EntryView = Backbone.View.extend({ model: TS.EntryModel, className: 'entry', events: { "dblclick div.mark" : "editMark", "dblclick div.timestamp" : "editTimestamp", "keypress .mark-input" : "updateOnEnter", "keypress .timestamp-input" : "updateOnEnter" }, initialize: function() { _.bindAll(this, 'render', 'close', 'editTImestamp', 'editMark', 'updateOnEnter'); this.model.bind('change', this.render); this.model.view = this; }, render: function() { $(this.el).html(ich.entryTemplate(this.model.toJSON())); return this; }, editMark: function() { $(this.el).addClass('edit-mark'); this.$('.mark-input').focus(); return this; }, editTimestamp: function() { $(this.el).addClass('edit-timestamp'); this.$('timestamp-input').focus(); return this; }, close: function() { this.model.save({ mark: this.$('.mark-input').val(), timestamp: this.$('.timestamp-input').val()}); $(this.el).removeClass('edit-mark edit-timestamp'); this.render(); }, updateOnEnter: function(e) { if(e.keyCode == 13) this.close(); } }); TS.EntryListView = Backbone.View.extend({ el: $("#entry-list"), events: { "keypress #new-entry-input" : "createNewEntryOnEnter" }, initialize: function() { _.bindAll(this, 'addOne', 'createNewEntry', 'render'); this.collection.bind('add', this.addOne); this.collection.bind('refresh', this.render); this.collection.view = this; this.entryContainer = this.$("#entries") }, addOne: function(entry) { if (!entry.view) { new TS.EntryView({model: entry}); } this.entryContainer.prepend(entry.view.render().el); }, createNewEntryOnEnter: function(e) { if (e.keyCode == 13) { // grab the mark data var entryMark = this.$("#new-entry-input").val(); // create the mark. Immediately fetch to get server-side timestamp this.collection.create({mark: entryMark, notes: '', timestamp: getUTCTimestamp()}).fetch(); } }, render: function() { this.entryContainer.empty(); this.collection.each(this.addOne); } }); TS.TimelineListView = Backbone.View.extend({ el: $("#timeline"), collection: TS.TimelineList, events: { "dblclick .timeline-id" : "editId", "dblclick .timeline-desc" : "editDesc", "keypress .timeline-id-input" : "updateOnEnter", "keypress .timeline-desc-input" : "updateOnEnter" }, initialize: function(options) { _.bindAll(this, 'render', 'renderOne', 'editId', 'editDesc', 'updateOnEnter'); if (options.initialTimelineId == undefined) { throw "Can not create a TimelineListView without an initial timeline." } else { this.selected = this.collection.get(options.initialTimelineId); } this.collection.bind('add', this.renderOne); this.collection.bind('refresh', this.render); }, renderOne: function(timeline) { this.$('.drop-menu-items').append( ich.timelineLinkTemplate(timeline.toJSON())); }, render: function() { // render the basic template $(this.el).html(ich.timelineTemplate(this.selected.toJSON())); // render the selection list _.each(this.collection.without([this.selected]), this.renderOne); }, editId: function() { $(this.el).addClass('edit-id'); this.$('.timeline-id-input').focus(); return this; }, editDesc: function() { $(this.el).addClass('edit-desc'); this.$('.timeline-desc-input').focus(); return this; }, close: function() { this.selected.save({ id: this.$('.timeline-id-input').val(), description: this.$('.timeline-desc-input').val()}); $(this.el).removeClass('edit-id edit-desc'); this.render(); }, updateOnEnter: function(e) { if (e.keyCode == 13) this.close(); } }); TS.UserView = Backbone.View.extend({ el: $("#user"), model: TS.UserModel, events: { 'dblclick .fullname': 'editFullname', 'keypress .fullname-input': 'updateOnEnter' }, initialize: function() { _.bindAll(this, 'render', 'close', 'editFullname', 'updateOnEnter'); this.model.bind('change', this.render); this.model.view = this; }, render: function() { $(this.el).html(ich.userTemplate(this.model.toJSON())); return this; }, editFullname: function() { $(this.el).addClass('edit-fullname'); this.$('.fullname-input').focus(); return this; }, close: function() { this.model.set({name: this.$('fullname-input').val()}); this.model.save(); $(this.el).removeClass('edit-fullname'); }, updateOnEnter: function(e) { if (e.keyCode == 13) this.close(); } }); TS.AppView = Backbone.View.extend({ el: $("body"), events: { 'click #timeline .drop-menu-items a': 'selectTimeline' }, initialize: function() { _.bindAll(this, 'initializeViews', 'loadInitialData'); appThis = this; // create the login dialog this.loginDialog = new TS.LoginView if (window.bootstrap) { this.initializeData(window.bootstrap()) } else { // this is async (waiting for user input) this.loginDialog.authenticate(function() { appThis.initializeData(appThis.loadInitialData())}); } }, initializeData: function(data) { // create user data this.user = {}; this.user.model = new TS.UserModel(data.user); this.user.view = new TS.UserView({model: this.user.model}); // create timeline models from the bootstrapped data var tlModels = _.map(data.timelines, function(timeline) { return new TS.TimelineModel(timeline); }); // create the timeline list collection this.timelines = {}; this.timelines.collection = new TS.TimelineList( tlModels, {user: this.user.model}); this.timelines.view = new TS.TimelineListView( {collection: this.timelines.collection, initialTimelineId: data.initialTimelineId}); // create entry models from the bootstrapped data var entryModels = _.map(data.entries, function(entry) { return new TS.EntryModel(entry); }); // create the entry collection this.entries = {}; this.entries.collection = new TS.EntryList(entryModels, {timeline: this.timelines.view.selected}); this.entries.view = new TS.EntryListView( {collection: this.entries.collection}); // render views this.user.view.render(); this.timelines.view.render(); this.entries.view.render(); }, loadInitialData: function() { // assume we are authenticated var username = $("#username-input").val(); // hackish var data = jQuery.parseJSON($.ajax({ url: '/ts_api/app/user_summary/' + username, async: false}).responseText); data.initialTimelineId = data.timelines[0].id; data.entries = jQuery.parseJSON($.ajax({ url: '/ts_api/entries/' + username + '/' + data.initialTimelineId, async: false}).responseText); return data; } }); TS.LoginView = Backbone.View.extend({ el: $("#login"), events: { "keypress #password-input" : "loginOnEnter", "click #login-button a" : "doLogin" }, initialize: function() { _.bindAll(this, 'authenticate', 'doLogin', 'hide', 'loginOnEnter', 'show'); }, action: function() {}, authenticate: function(nextAction) { this.action = nextAction; this.show(); }, doLogin: function(){ var viewThis = this; var name = this.$("#username-input"); var pwd = $("#password-input"); // call the API via AJAX $.ajax({ url: "/ts_api/login", processData: false, data: JSON.stringify({username: name.val(), password: pwd.val()}), type: "POST", async: false, error: function(jqXHR, textStatus, error) { // assuming bad credentials (possible server error or bad request, // we should check that, FIXME var tips = $(".validate-tips"); tips.text("Incorrect username/password combination."); tips.slideDown(); }, success: function(data, textStatus, jqXHR) { viewThis.hide(); viewThis.action(); } }); }, hide: function() { $(this.el).addClass('hidden'); }, show: function() { $(this.el).removeClass('hidden'); this.$("#username-input").focus(); }, loginOnEnter: function(e) { if (e.keyCode == 13) { this.doLogin(); } } }); TS.app = new TS.AppView; }) function getUTCTimestamp() { var d = new Date(); function pad(n){return n<10 ? '0'+n : n} return d.getUTCFullYear()+'-' + pad(d.getUTCMonth()+1)+'-' + pad(d.getUTCDate())+'T' + pad(d.getUTCHours())+':' + pad(d.getUTCMinutes())+':' + pad(d.getUTCSeconds())+'Z'; }