* Added UUIDs to `ts_entry` records. Updated `ts_json:construct_record` to respond to `uuid` member properties if present. UUIDs are not required by the strict parsing functions in `ts_json` because the client will make a request with no UUID if it is a purely new timestamp. IN fact, this is the normal case. The UUID is only present when another tool is syncing its copy of this timeline wand adding entries that it has created and assigned UUIDs to. * `ts_entry:new` will create a UUID for a new entry if it does not already have one. * Restructured the build process to put all build artifacts into a dedicated `build` subdirectory, instead of mising them in an amongst the source code. * Added the `uuid` module to the project. It can be found at https://gitorious.org/avtobiff/erlang-uuid * Rewrote asset URLs to use relative paths instead of absolute paths. Relative paths are correct in this case, becuase assets always live alongside the HTML pages. This change was needed to accomodate the new organization of the JDB Labs dev environment, where all projects live under subdirectories of the same virtual server instead of subdomains. * Tweaked the timestamp entry fields in the web UI to save when the field is blurred, not just when <Enter> or <Ctrl>-<Enter> is pressed (though those still work).
802 lines
26 KiB
JavaScript
802 lines
26 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
|
|
* - timestamp
|
|
*/
|
|
TS.EntryModel = Backbone.Model.extend({
|
|
|
|
get: function(attribute) {
|
|
if (attribute == "timestamp") {
|
|
if (!this.timestampDate) {
|
|
this.timestampDate = new Date(
|
|
Backbone.Model.prototype.get.call(this, attribute));
|
|
}
|
|
return this.timestampDate;
|
|
} else {
|
|
return Backbone.Model.prototype.get.call(this, attribute);
|
|
}
|
|
},
|
|
|
|
set: function(attributes, options) {
|
|
var attrsToSet = {}
|
|
_.each(attributes, function(val, key) {
|
|
if (key == "timestamp") {
|
|
if (val instanceof Date) {
|
|
this.timestampDate = val;
|
|
attrsToSet.timestamp = dateToJSON(val);
|
|
} else {
|
|
this.timestampDate = new Date(val);
|
|
attrsToSet.timestamp = dateToJSON(this.timestampDate);
|
|
}
|
|
} else {
|
|
attrsToSet[key] = val;
|
|
}
|
|
});
|
|
|
|
return Backbone.Model.prototype.set.call(this, attrsToSet, options);
|
|
}
|
|
});
|
|
|
|
/* 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.timelineModel == undefined) {
|
|
throw "Cannot create an EntryList without a TimelineModel reference."
|
|
} else { this.timelineModel = options.timelineModel; }
|
|
|
|
_.bindAll(this, "url");
|
|
},
|
|
|
|
url: function() {
|
|
return "/ts_api/entries/" + this.timelineModel.get('user_id') + "/"
|
|
+ this.timelineModel.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" : "saveOnEnter",
|
|
"keypress .timestamp-input" : "saveOnEnter",
|
|
"keypress .notes-input" : "saveOnCtrlEnter",
|
|
"blur .mark-input" : "save",
|
|
"blur .timestamp-input" : "save",
|
|
"blur .notes-input" : "save"
|
|
},
|
|
|
|
initialize: function(options) {
|
|
_.bindAll(this, 'render', 'editTImestamp', 'editMark', 'update',
|
|
'saveOnEnter', 'saveOnCtrlEnter', 'getViewModel',
|
|
'renderNotes', 'showNotes', 'hideNotes', 'save');
|
|
|
|
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;
|
|
},
|
|
|
|
/** Save and close editable fields. */
|
|
save: function() {
|
|
this.model.save({
|
|
mark: this.$('.mark-input').val(),
|
|
timestamp: new Date(this.$('.timestamp-input').val()),
|
|
notes: this.$('.notes-input').val()});
|
|
|
|
this.$('.notes-text').html(this.renderNotes(this.model.get('notes')));
|
|
$(this.el).removeClass('edit-mark edit-timestamp edit-notes');
|
|
},
|
|
|
|
/** Event handler for keypresses on entry input fields. */
|
|
saveOnEnter: function(e) {
|
|
if(e.keyCode == 13) { this.save(); }
|
|
},
|
|
|
|
saveOnCtrlEnter: function(e) {
|
|
if (e.keyCode == 10) { this.save(); }
|
|
},
|
|
|
|
/**
|
|
* 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 = model.get('timestamp');
|
|
var d2, diff;
|
|
var day, hr, min;
|
|
|
|
// if no next model, assume it's an onoing task
|
|
if (nextModel) { d2 = 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) {
|
|
// exclude if any exclusion RegExps match
|
|
var excluded = _.any(this.entryExclusions,
|
|
function(exclusion) { return exclusion.test(entry.get("mark"))});
|
|
|
|
// create the view if it does not exist
|
|
if (!entry.view) { new TS.EntryView(
|
|
{model: entry, markdownConverter: this.markdownConverter}); }
|
|
entry.view.nextModel = nextEntry
|
|
|
|
// render the element
|
|
var el = entry.view.render().el;
|
|
|
|
// add it to the container after the topmost separator ("Today")
|
|
this.topSeparator.after(el);
|
|
|
|
// hide it if excluded
|
|
if (excluded) {
|
|
$(el).fadeOut('slow');
|
|
$(el).addClass('excluded');
|
|
}
|
|
},
|
|
|
|
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: new Date()}).fetch();
|
|
|
|
// clear the input for the next entry
|
|
this.$("#new-entry-input").val("");
|
|
|
|
}
|
|
},
|
|
|
|
render: function() {
|
|
|
|
// get our user exclusions
|
|
var userExclusions = TS.app.user.model.get("entry_exclusions") || [];
|
|
|
|
// get the current timeline exclusions
|
|
var timelineExclusions =
|
|
this.collection.timelineModel.get("entry_exclusions") || [];
|
|
|
|
// turn them into RegExps and store them
|
|
this.entryExclusions = _.map(
|
|
userExclusions.concat(timelineExclusions),
|
|
function(exclusion) { return new RegExp(exclusion)} );
|
|
|
|
// clear existing elements in the view container
|
|
this.entryContainer.empty();
|
|
|
|
// last day we have printed a separator for
|
|
var today = new Date()
|
|
var currentDay = this.collection.at(0) ?
|
|
this.collection.at(0).get("timestamp"): today;
|
|
|
|
// add the top-most day separator
|
|
this.topSeparator = ich.daySeparatorTemplate({
|
|
separatorLabel: this.formatDaySeparator(today, today) });
|
|
this.entryContainer.prepend(this.topSeparator);
|
|
|
|
// iterate through the collection and render the elements.
|
|
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);
|
|
|
|
// we are rendering buttom up, which means we need to insert the
|
|
// day separator before the first entry of the *next* period
|
|
if (currentDay.getDate() != entry.get("timestamp").getDate()) {
|
|
this.topSeparator.after(ich.daySeparatorTemplate(
|
|
{separatorLabel: this.formatDaySeparator(today, currentDay)}));
|
|
currentDay = entry.get('timestamp');
|
|
}
|
|
|
|
this.renderOne(entry, nextEntry);
|
|
}
|
|
},
|
|
|
|
formatDaySeparator: function(today, labelDay) {
|
|
|
|
var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
|
|
'Friday', 'Saturday'];
|
|
var months = ['January', 'February', 'March', 'April', 'May',
|
|
'June', 'July', 'August', 'September', 'October',
|
|
'November', 'December'];
|
|
|
|
var yearDiff = today.getFullYear() - labelDay.getFullYear();
|
|
var monthDiff = today.getMonth() - labelDay.getMonth();
|
|
var dayDiff = today.getDate() - labelDay.getDate();
|
|
|
|
// more than a calendar year old
|
|
if (yearDiff > 0) {
|
|
|
|
return days[labelDay.getDay()] + ", " +
|
|
months[labelDay.getMonth()] + " " + labelDay.getDate() +
|
|
", " + labelDay.getFullYear(); }
|
|
|
|
// same calendar year, more than a week ago
|
|
else if (monthDiff > 0 || dayDiff > 7) {
|
|
|
|
return days[labelDay.getDay()] + ", " +
|
|
months[labelDay.getMonth()] + " " + labelDay.getDate(); }
|
|
|
|
// less than a week ago, more than yesterday
|
|
else if (dayDiff > 1) {
|
|
return "Last " + days[labelDay.getDay()]; }
|
|
|
|
// yesterday
|
|
else if (dayDiff == 1) { return "Yesterday"; }
|
|
|
|
// today
|
|
else if (dayDiff == 0) { return "Today"; }
|
|
|
|
}
|
|
});
|
|
|
|
TS.TimelineListView = Backbone.View.extend({
|
|
el: $("#timeline"),
|
|
|
|
collection: TS.TimelineList,
|
|
|
|
events: {
|
|
"dblclick .timeline-id" : "editId",
|
|
"dblclick .timeline-desc" : "editDesc",
|
|
"keypress .timeline-id-input" : "saveOnEnter",
|
|
"keypress .timeline-desc-input" : "saveOnEnter",
|
|
"click .new-timeline-link" : "showNewTimelineDialog"
|
|
},
|
|
|
|
initialize: function(options) {
|
|
_.bindAll(this, 'render', 'renderOne', 'editId',
|
|
'editDesc', 'saveOnEnter', 'save');
|
|
|
|
if (options.initialTimelineId == undefined) {
|
|
throw "Can not create a TimelineListView without an initial timeline."
|
|
} else {
|
|
this.selectedModel = 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.selectedModel.toJSON()));
|
|
|
|
// render the selection list
|
|
_.each(this.collection.without([this.selectedModel]), 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;
|
|
},
|
|
|
|
save: function() {
|
|
this.selectedModel.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();
|
|
},
|
|
|
|
saveOnEnter: function(e) {
|
|
if (e.keyCode == 13) { this.save(); }
|
|
}
|
|
|
|
});
|
|
|
|
TS.UserView = Backbone.View.extend({
|
|
|
|
el: $("#user"),
|
|
|
|
model: TS.UserModel,
|
|
|
|
events: {
|
|
'dblclick .fullname': 'editFullname',
|
|
'keypress .fullname-input': 'saveOnEnter'
|
|
},
|
|
|
|
initialize: function() {
|
|
_.bindAll(this, 'render', 'save', 'editFullname', 'saveOnEnter');
|
|
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;
|
|
},
|
|
|
|
save: function() {
|
|
this.model.set({name: this.$('fullname-input').val()});
|
|
this.model.save();
|
|
$(this.el).removeClass('edit-fullname');
|
|
},
|
|
|
|
saveOnEnter: function(e) {
|
|
if (e.keyCode == 13) this.save();
|
|
}
|
|
});
|
|
|
|
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,
|
|
{timelineModel: this.timelines.view.selectedModel});
|
|
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);
|
|
|
|
// look for the last used timeline, default to first timeline
|
|
data.initialTimelineId =
|
|
data.user.last_timeline || 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.selectedModel = tl;
|
|
|
|
// set the timeline on the EntryList
|
|
this.entries.collection.timelineModel = tl;
|
|
|
|
// update the last_timeline field of the user model
|
|
this.user.model.set({last_timeline: tl.get('id')});
|
|
this.user.model.save();
|
|
|
|
// 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: dateToJSON(new Date())});
|
|
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 dateToJSON(d) {
|
|
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';
|
|
}
|