* Added personal VIM ide extension. * Implemented timeline selection (resolves D0007) * Slightly restyled the new timeline button and timeline list menu.
		
			
				
	
	
		
			684 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			684 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// 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',
 | 
						|
 | 
						|
        notesCache: false,
 | 
						|
 | 
						|
        events: {
 | 
						|
            "click img.expand-entry"    : "showNotes",
 | 
						|
            "click img.collapse-entry"  : "hideNotes",
 | 
						|
            "dblclick div.mark"         : "editMark",
 | 
						|
            "dblclick div.timestamp"    : "editTimestamp",
 | 
						|
            "dblclick div.notes"        : "editNotes",
 | 
						|
            "keypress .mark-input"      : "updateOnEnter",
 | 
						|
            "keypress .timestamp-input" : "updateOnEnter",
 | 
						|
            "keypress .notes-input"     : "updateOnCtrlEnter",
 | 
						|
            "blur .mark-input"          : "close",
 | 
						|
            "blur .timestamp-input"     : "close",
 | 
						|
            "blur .notes-input"         : "close"
 | 
						|
        },
 | 
						|
 | 
						|
        initialize: function(options) {
 | 
						|
            _.bindAll(this, 'render', 'close', 'editTImestamp', 'editMark',
 | 
						|
                'update', 'updateOnEnter', 'updateOnCtrlEnter', 'getViewModel',
 | 
						|
                'renderNotes', 'showNotes', 'hideNotes');
 | 
						|
 | 
						|
            this.markdownConverter = options.markdownConverter;
 | 
						|
 | 
						|
            this.model.bind('change', this.update);
 | 
						|
            this.model.view = this;
 | 
						|
 | 
						|
            this.nextModel = options.nextModel;
 | 
						|
        },
 | 
						|
 | 
						|
        /**
 | 
						|
         * Refresh the display based on the model replacing the existing
 | 
						|
         * HTML content. Add new `blur` listeners to the input fields.
 | 
						|
         */
 | 
						|
        render: function() {
 | 
						|
            // render the HTML
 | 
						|
            $(this.el).html(ich.entryTemplate(this.getViewModel()));
 | 
						|
 | 
						|
            // invalidate the notes display cache
 | 
						|
            this.notesCache = false;
 | 
						|
 | 
						|
            return this;
 | 
						|
        },
 | 
						|
 | 
						|
        /**
 | 
						|
         * Refresh the display based on the model using the existing DOM
 | 
						|
         * elements.
 | 
						|
         */
 | 
						|
        update: function() {
 | 
						|
            var data = this.getViewModel();
 | 
						|
            this.$('.mark span').text(data.mark);
 | 
						|
            this.$('.mark-input').val(data.mark);
 | 
						|
            this.$('.timestamp').text(data.start);
 | 
						|
            this.$('.timestamp-input').val(data.timestamp);
 | 
						|
            this.$('.duration').text(data.duration);
 | 
						|
            this.$('.notes-text').html(this.renderNotes(data.notes));
 | 
						|
            this.$('.notes-input').val(data.notes);
 | 
						|
            return this;
 | 
						|
        },
 | 
						|
 | 
						|
        renderNotes: function(source) {
 | 
						|
            if (!this.notesCache) {
 | 
						|
                this.notesCache = this.markdownConverter.makeHtml(source);
 | 
						|
            }
 | 
						|
 | 
						|
            return this.notesCache
 | 
						|
        },
 | 
						|
 | 
						|
        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;
 | 
						|
        },
 | 
						|
 | 
						|
        editNotes: function() {
 | 
						|
            // invalidate notes HTML cache
 | 
						|
            this.notesCache = false;
 | 
						|
 | 
						|
            // show notes textarea, hide display
 | 
						|
            $(this.el).addClass('edit-notes');
 | 
						|
 | 
						|
            // focus input
 | 
						|
            this.$('.notes-input').focus();
 | 
						|
            return this;
 | 
						|
        },
 | 
						|
 | 
						|
        /**
 | 
						|
         * Translate the model data into a form suitable to be displayed.
 | 
						|
         * @return a map including display-able `start` and `duration` values.
 | 
						|
         */
 | 
						|
        getViewModel: function() {
 | 
						|
            var data = this.model.toJSON();
 | 
						|
 | 
						|
            // create start and duration values
 | 
						|
            var tsDate = new Date(data.timestamp);
 | 
						|
            data.start = this.formatStart(tsDate);
 | 
						|
            data.duration = this.formatDuration(this.model, this.nextModel);
 | 
						|
            data.notes = data.notes ? data.notes : '*No notes for this entry.*';
 | 
						|
            return data;
 | 
						|
        },
 | 
						|
 | 
						|
        /** Close editable fields.  */
 | 
						|
        close: function() {
 | 
						|
            $(this.el).removeClass('edit-mark edit-timestamp edit-notes');
 | 
						|
        },
 | 
						|
 | 
						|
        /** Persist changes in input fields.  */
 | 
						|
        save: function() {
 | 
						|
            this.model.save({
 | 
						|
                mark: this.$('.mark-input').val(), 
 | 
						|
                timestamp: this.$('.timestamp-input').val(),
 | 
						|
                notes: this.$('.notes-input').val()});
 | 
						|
        },
 | 
						|
 | 
						|
        /** Event handler for keypresses on entry input fields. */
 | 
						|
        updateOnEnter: function(e) {
 | 
						|
            if(e.keyCode == 13) { this.save(); this.close(); }
 | 
						|
        },
 | 
						|
 | 
						|
        updateOnCtrlEnter: function(e) {
 | 
						|
            if (e.keyCode == 10) { this.save(); this.close(); }
 | 
						|
        },
 | 
						|
 | 
						|
        /**
 | 
						|
         * Get the display-able start time from the entry timestamp.
 | 
						|
         * @param startDate a Date object, the entry timestamp.
 | 
						|
         * @return display-able start time in HH:MM format.
 | 
						|
         */
 | 
						|
        formatStart: function(startDate) {
 | 
						|
            /* Code is written for both 24hr and 12hr formats. I still need to
 | 
						|
             * create a mechanism for selecting between them. For now, use the
 | 
						|
             * 12hr format. */
 | 
						|
 | 
						|
            // 24 hour
 | 
						|
            // return startDate.getHours() + ":" + startDate.getMinutes();
 | 
						|
 | 
						|
            // 12 hour
 | 
						|
            var hour = startDate.getHours() % 12;
 | 
						|
            return (hour == 0 ? 12 : hour) + ":" + startDate.getMinutes() +
 | 
						|
                " " + (startDate.getHours() > 11 ? "pm" : "am");
 | 
						|
        },
 | 
						|
 | 
						|
        /**
 | 
						|
         * Get the duration of the entry based on this entry's timestamp and
 | 
						|
         * and the next entry's timestamp in a display-able form. If nextModel
 | 
						|
         * is `null` or `undefined` it is assumed that `model` is the most
 | 
						|
         * recent model and duration is calculated against the current time.
 | 
						|
         *
 | 
						|
         * @param model EntryModel representing this entry.
 | 
						|
         * @param nextModel EntryModel representing the next entry.
 | 
						|
         * @return the duration between model and nextModel, formatted for 
 | 
						|
         *         display: `Xd Yhr Zm`. */
 | 
						|
        formatDuration: function(model, nextModel) {
 | 
						|
            var d1 = new Date(model.get('timestamp'));
 | 
						|
            var d2, diff;
 | 
						|
            var day, hr, min;
 | 
						|
 | 
						|
            // if no next model, assume it's an onoing task
 | 
						|
            if (nextModel) { d2 = new Date(nextModel.get('timestamp')); }
 | 
						|
            else { d2 = new Date(); }
 | 
						|
 | 
						|
            diff= d2.getTime() - d1.getTime();
 | 
						|
            day = Math.floor(diff / 86400000); // milliseconds in a day
 | 
						|
            diff %= 86400000;
 | 
						|
            
 | 
						|
            hr = Math.floor(diff / 3600000); // millis in an hour
 | 
						|
            diff %= 3600000;
 | 
						|
 | 
						|
            min = Math.floor(diff / 60000); // millis in a minute
 | 
						|
 | 
						|
            return (day > 0 ? day + "d " : "") +
 | 
						|
                   (hr  > 0 ? hr + "hr " : "") +
 | 
						|
                   min + "m ";
 | 
						|
        },
 | 
						|
 | 
						|
        showNotes: function() {
 | 
						|
            if (!this.notesCache) {
 | 
						|
                this.$('.notes-text').html(
 | 
						|
                    this.renderNotes(this.model.get('notes')))
 | 
						|
            }
 | 
						|
 | 
						|
            this.$('.notes').slideDown();
 | 
						|
            $(this.el).addClass('show-notes');
 | 
						|
        },
 | 
						|
 | 
						|
        hideNotes: function() {
 | 
						|
            this.$('.notes').slideUp();
 | 
						|
            $(this.el).removeClass('show-notes');
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    TS.EntryListView = Backbone.View.extend({
 | 
						|
        
 | 
						|
        el: $("#entry-list"),
 | 
						|
 | 
						|
        events: {
 | 
						|
            "keypress #new-entry-input"    : "createNewEntryOnEnter"
 | 
						|
        },
 | 
						|
 | 
						|
        initialize: function() {
 | 
						|
            _.bindAll(this, 'addOne', 'createNewEntry', 'render', 'renderOne');
 | 
						|
            this.collection.bind('add', this.addOne);
 | 
						|
            this.collection.bind('refresh', this.render);
 | 
						|
            this.collection.view = this;
 | 
						|
            this.entryContainer = this.$("#entries")
 | 
						|
            this.markdownConverter = new Showdown.converter();
 | 
						|
        },
 | 
						|
 | 
						|
        addOne: function(entry) {
 | 
						|
            var lastEntry = this.collection.at(this.collection.length - 2);
 | 
						|
            lastEntry.view.nextModel = entry;
 | 
						|
            lastEntry.view.update();
 | 
						|
            this.renderOne(entry, null);
 | 
						|
        },
 | 
						|
 | 
						|
        renderOne: function(entry, nextEntry) {
 | 
						|
            if (!entry.view) { new TS.EntryView(
 | 
						|
                    {model: entry, markdownConverter: this.markdownConverter}); }
 | 
						|
            entry.view.nextModel = nextEntry
 | 
						|
            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();
 | 
						|
 | 
						|
                // clear the input for the next entry
 | 
						|
                this.$("#new-entry-input").val("");
 | 
						|
 | 
						|
            } 
 | 
						|
        },
 | 
						|
 | 
						|
        render: function() {
 | 
						|
            this.entryContainer.empty();
 | 
						|
            for (var i = 0, len = this.collection.length; i < len; i++) {
 | 
						|
                var entry = this.collection.at(i);
 | 
						|
                var nextEntry = (i + 1 < len ? this.collection.at(i + 1) : null);
 | 
						|
 | 
						|
                this.renderOne(entry, nextEntry);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    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",
 | 
						|
            "click .new-timeline-link"      : "showNewTimelineDialog"
 | 
						|
        },
 | 
						|
 | 
						|
        initialize: function(options) {
 | 
						|
            _.bindAll(this, 'render', 'renderOne', 'editId',
 | 
						|
                'editDesc', 'updateOnEnter', 'close');
 | 
						|
 | 
						|
            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();
 | 
						|
        },
 | 
						|
 | 
						|
        showNewTimelineDialog: function() {
 | 
						|
            TS.app.newTimelineDialog.show();
 | 
						|
        },
 | 
						|
 | 
						|
        updateOnEnter: function(e) {
 | 
						|
            if (e.keyCode == 13) { this.close(); this.save() }
 | 
						|
        }
 | 
						|
 | 
						|
    });
 | 
						|
 | 
						|
    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',
 | 
						|
                'selectTimeline');
 | 
						|
 | 
						|
            appThis = this;
 | 
						|
 | 
						|
            // create the login dialog
 | 
						|
            this.loginDialog = new TS.LoginView
 | 
						|
 | 
						|
            // initialize data, either from boostrapped data, or via user login
 | 
						|
            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 the new timeline dialog
 | 
						|
            this.newTimelineDialog = new TS.NewTimelineView(
 | 
						|
                {timelineCollection: this.timelines.collection});
 | 
						|
 | 
						|
            // 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;
 | 
						|
        },
 | 
						|
 | 
						|
        selectTimeline: function(e) {
 | 
						|
            if (e) {
 | 
						|
                // get the timeline model
 | 
						|
                var tl = this.timelines.collection.get(e.srcElement.text);
 | 
						|
 | 
						|
                // set the on the timeline view
 | 
						|
                this.timelines.view.selected = tl;
 | 
						|
 | 
						|
                // set the timeline on the EntryList
 | 
						|
                this.entries.collection.timeline = tl;
 | 
						|
 | 
						|
                // refresh TimelineListView
 | 
						|
                this.timelines.view.render();
 | 
						|
 | 
						|
                // refresh EntryList records
 | 
						|
                this.entries.collection.fetch()
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
    });
 | 
						|
 | 
						|
    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.NewTimelineView = Backbone.View.extend({
 | 
						|
        el: $("#new-timeline"),
 | 
						|
 | 
						|
        events: {
 | 
						|
            "click #new-timeline-create a"  : "createTimeline",
 | 
						|
            "click #new-timeline-cancel a"  : "hide"
 | 
						|
        },
 | 
						|
 | 
						|
        initialize: function(options) {
 | 
						|
            _.bindAll(this, 'createTimeline', 'hide', 'show');
 | 
						|
 | 
						|
            if (options.timelineCollection == undefined) {
 | 
						|
                throw "Can not create the NewTimelineView without the timeline collection."
 | 
						|
            } else {
 | 
						|
                this.timelineCollection = options.timelineCollection;
 | 
						|
            }
 | 
						|
 | 
						|
        },
 | 
						|
 | 
						|
        createTimeline: function() {
 | 
						|
            var timelineId = this.$("#new-timeline-id").val();
 | 
						|
            var timelineDesc = this.$("#new-timeline-desc").val();
 | 
						|
            this.timelineCollection.create(
 | 
						|
                {id: timelineId, description: timelineDesc, 
 | 
						|
                 created: getUTCTimestamp()});
 | 
						|
            this.hide();
 | 
						|
        },
 | 
						|
 | 
						|
        hide: function() { $(this.el).addClass('hidden'); },
 | 
						|
 | 
						|
        show: function() {
 | 
						|
            this.$("#new-timeline-id").val("");
 | 
						|
            this.$("#new-timeline-desc").val("");
 | 
						|
            $(this.el).removeClass('hidden');
 | 
						|
            this.$("#new-timeline-id").focus();
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    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';
 | 
						|
}
 | 
						|
 |