* Changed the cookie Path value to allow the cookie to be reused for the domain, not just ths `/ts_api` path. This allows the user to refresh the page and reuse their existing session as long as it is not stale. * Fixed a bug in the `ts_json:ejson_to_record_strict/2` function. It was expecting a record out of `ts_json:ejson_to_record/2` but that function returns a tuple with the record and extended data. Because of the way `ejson_to_record_strict` uses case statements to check for specific values it was still passing the parsed record and data through, but all the checks were being bypassed. * Fixed bugs in the `index.yaws` bootstrap code for the case where the user already has a valid session. * Added `urlRoot` functions to the Backbone model definitions. * Changed the behavior of the new entry creation method. We were trying to fetch just updated attributes from the server for the model we created, but we were pulling all the entries due to the URL backbone was using. This led to the new client-side model having all the previous entry models as attributes. Ideally we would fix the fetch so that only the one model is requested from the server, but we run into a catch-22 because the lookup id is not know by the client as it is generated on the server-side. For now I have changed this behavior so that we still pull all entries, but we pull them into the collection. The collection is then smart enough to update the entries that have changed (namely the new one). The server returns the newly created entry attributes in response to the POST request that the client makes initially, so when I have more time to work on this I plan to do away with the fetch after create, and just pull in the data from the server's response. * Changed formatting.
727 lines
26 KiB
JavaScript
727 lines
26 KiB
JavaScript
/**
|
|
* # `ts.js`: TimeStamper Web UI Client-side
|
|
* @author Jonathan Bernard <jdb@jdb-labs.com>
|
|
**/
|
|
|
|
// TimeStamper namespace
|
|
var TS = {};
|
|
|
|
/* Setup after the document is ready for manipulation. */
|
|
$(document).ready(function(){
|
|
|
|
// ======== DEFINE MODELS ========//
|
|
|
|
/* Entry model.
|
|
* Attributes
|
|
* - user_id
|
|
* - timeline_id
|
|
* - id
|
|
* - mark
|
|
* - uuid
|
|
* - notes
|
|
* - timestamp
|
|
*/
|
|
TS.EntryModel = Backbone.Model.extend({
|
|
|
|
initialize: function(attrs, options) {
|
|
_.bindAll(this, 'get', 'set', 'urlRoot'); },
|
|
|
|
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); },
|
|
|
|
urlRoot: function() { return '/ts_api/entries/' + this.get('user_id') +
|
|
'/' + this.get('timeline_id') + '/'; } });
|
|
|
|
/* Timeline model.
|
|
* Attributes:
|
|
* - user_id
|
|
* - id
|
|
* - description
|
|
* - created
|
|
*/
|
|
TS.TimelineModel = Backbone.Model.extend({
|
|
urlRoot: function() {
|
|
return '/ts_api/timelines/' + this.get('user_id') + '/'; },
|
|
|
|
initialze: function(attrs, options) { _.bindAll(this, 'urlRoot'); } });
|
|
|
|
/* User model.
|
|
* Attributes:
|
|
* - id
|
|
* - name
|
|
* - email
|
|
* - join_date
|
|
*/
|
|
TS.UserModel = Backbone.Model.extend({
|
|
url: function() { return '/ts_api/users/' + this.get('id'); },
|
|
|
|
initialize: function(attrs, options) { _.bindAll(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", "create"); },
|
|
|
|
/*create: function(model, options) {
|
|
if (!(model instanceof Backbone.Model)) {
|
|
model.user_id = this.timelineModel.get('user_id');
|
|
model.timeline_id = this.timelineModel.get('timeline_id'); }
|
|
else {
|
|
model.set('user_id') = this.timelineModel.get('user_id')
|
|
},*/
|
|
|
|
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()});
|
|
|
|
this.collection.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) {
|
|
|
|
TS.app = this;
|
|
|
|
// 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(); } });
|
|
|
|
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';
|
|
}
|