Implementing new client design using Backbone.js.

This commit is contained in:
Jonathan Bernard
2011-04-15 13:26:55 -05:00
parent d0aab2d0c6
commit e10ec26a21
12 changed files with 1728 additions and 1057 deletions

View File

@ -1,54 +1,214 @@
var user = [];
var timelines = [];
var activeTimeline = [];
var entries = [];
var newEntryBar;
var moreEntriesbar;
var currentEntryOffset = 0;
var loadLength = 20;
var loginTop;
// TimeStamper namespace
var TS = {};
/* Setup after the document is ready for manipulation. */
$(document).ready(function(){
// looked up once and remembered
newEntryBar = $("#new-entry");
moreEntriesBar = $("#more-entries");
// ======== DEFINE MODELS ========//
/* Entry model.
* Attributes
* - id
* - mark
* - notes
* - start
*/
TS.Entry = Backbone.Model.extend({
// wire the login dialog using jQuery UI
$("#login-dialog").dialog({
autoOpen: false,
height: 300,
width: 300,
modal: true,
buttons: {
"Sign Up": function(){signup()},
Login: function(){login()} }
});
// TODO: add a hook to AJAX requests to check for 401 unauth
// and re-display the login dialog.
/* Timeline model.
* Attributes:
* - id
* - desc
* - created
*/
TS.Timeline = Backbone.Model.extend({
// try to load user information for an authenticated user
$.ajax({
url: "/ts_api/users/",
type: "GET",
});
error: function(jqXHR, textStatus, error) {
// assume there is no authenticated user, show login dialog
$("#login-dialog").dialog("open"); },
/* User model.
* Attributes:
* - username
* - fullname
* - email
* - join_date
*/
TS.User = Backbone.Model.extend({
success: function(data, textStatus, jqXHR) {
// load the user information
loadUser(data.user.username); }});
});
TS.EntryList = Backbone.Collection.extend({
model: TS.Entry,
comparator: function(entry) { return entry.get('timestamp'); },
initialize: function() {
_.bindAll(this, "ur");
},
url: function() {
return "/entries/" + TS.currentUser.get('username') + "/" + this.timeline.get('id');
}
});
TS.TimelineList = Backbone.Collection.extend({
model: TS.Timeline,
url: function() {
return "/timelines/" + TS.currentUser.get('username');
}
});
// ======== DEFINE VIEWS ========//
/* Entry view
*/
TS.EntryView = Backbone.View.extend({
model: TS.Entry,
events: {
"dblclick div.mark" : "editMark",
"dblclick div.timestamp" : "editTimestamp",
"keypress .mark-input" : "updateOnEnter"
"keypress .timestamp-input" : "updateOnEnter"
},
initialize: function() {
_.bindAll(this, 'render', 'close');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
$(this.el).html(ich.entry(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');
},
updateOnEnter: function(e) {
if(e.keyCode == 13) this.close();
}
});
TS.TimelineView = Backbone.View.extend({
el: $("#timeline"),
model: TS.Timeline,
events: {
"dblclick .timeline-id" : "editId",
"dblclick .timeline-desc" : "editDesc",
"keypress .timeline-id-input" : "updateOnEnter",
"keypress .timeline-desc-input" : "updateOnEnter"
},
initialize: function() {
_.bindAll(this, 'render', 'close');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
this.$('.timeline-id').html('( ' +
this.model.get('id') + ' )');
this.$('.timeline-desc').text(this.model.get('desc'));
return this;
},
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.model.save({
id: this.$('timeline-id-input').val(),
desc: this.$('.timeline-desc-input').val()});
$(this.el).removeClass('.edit-id .edit-desc');
},
updateOnEnter: function(e) {
if (e.keyCode == 13) this.close();
}
});
TS.UserView = Backbone.View.extend({
el: $("user"),
model: TS.User,
events: {
'dblclick .fullname': 'editFullname',
'keypress .fullname-input': 'updateOnEnter'
},
initialize: function() {
_.bindAll(this, 'render', 'close');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
this.$('.fullname').text(this.model.get('name'));
this.$('.username').text(this.model.get('id'));
},
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 (keyCode == 13) this.close();
}
});
TS.currentUser = {};
TS.currentUser.model = new TS.User({
id: $("#user .username").text(),
name: $("#user .fullname").text() });
TS.currentUser.view = new TS.UserView(TS.currentUser.model);
})
/* Read the user's credentials from the login form and perform
* an AJAX request to the API to set the session cookie. */
function login() {
// lookup the login dialog elements
var name = $("#login-name");
@ -71,310 +231,6 @@ function login() {
},
success: function(data, textStatus, jqXHR) {
// load the user information and hide the login dialog
loadUser(name.val());
$("#login-dialog").dialog("close");
TS.currentUser = new TS.User(); // TODO
}});
}
function toggleSignUp(event) {
var signUpCB = $("#signup-checkbox");
if (signUpCB.attr("checked")) {
loginTop = $("#login-dialog").dialog("widget").offset().top;
$("#login-dialog").animate({height: 350}, 500);
$("#login-dialog").dialog("widget").animate({top: loginTop - 200}, 500);
$(".signup").slideDown("slow");
} else {
$("#login-dialog").animate({height: 180}, 500);
$("#login-dialog").dialog("widget").animate({top: loginTop}, 500);
$(".signup").slideUp("slow");
}
}
function signUp() {
}
/* End the current user session and expire any session credentials we
* have aquired. */
function logout(event) {
alert("TODO: log user out via AJAX.");
// TODO: wipe username, timeline, entry variables and displays
event.preventDefault();
}
/* Load and display the user's information and timelines. */
function loadUser(username) {
// call the user_summary API function
$.ajax({
url: "/ts_api/app/user_summary/" + username,
type: "GET",
success: function(data, textStatus, jqXHR) {
// set the user variable
user = data.user;
// set the timelines variable
timelines = data.timelines;
// update the user id display
$("#fullname").text(user.name);
$("#username").text("- " + user.username);
// pre-populate the editable user-info fields
$("#fullname-input").val(user.name);
$("#email-input").val(user.email);
// set the active timeline to the first in the list
// TODO: implement a mechanism to remember the last used timeline
// on the server side and respond to that here.
activeTimeline = timelines[0];
// update the timeline display
$("#timeline-name").text(activeTimeline.timeline_id + " |");
$("#timeline-desc").text(activeTimeline.description);
$("#timeline-desc-input").val(activeTimeline.description);
// TODO: populate the drop-down list for the available timeline
// choices
// load the entries for this timeline
loadEntries(user, activeTimeline, "new")
},
error: function(jqXHR, textStatus, error) {
// TODO
alert("TODO: handle error for user load.")
alert(jqXHR.responseText)
}
});
}
/* Read entries for a timeline. */
function loadEntries(user, timeline, order) {
// call the API list_entries function via AJAX
$.ajax({
url: "/ts_api/entries/" + user.username + "/"
+ timeline.timeline_id + "?order=asc&start="
+ currentEntryOffset + "&length=" + loadLength,
type: "GET",
success: function(data, textStatus, jqXHR) {
entries = data.entries;
if (order == "new") {
// reverse the list so that the oldest are first
entries.reverse()
// push the entries onto the page
displayNewerEntries(entries)
} else {
displayOlderEntries(entries)
}
// update our current offset
currentEntryOffset += loadLength;
},
error: function(jqXHR, textStatus, error) {
alert(jqXHR.responseText);
}
});
}
/* Push the entries onto the top of the entry display. This function assumes
* that the entries are sorted by id, ascending (0, 1, 2). Each new entry goes
* on top, pushing down all existing entries. So the first entry in the
* argument list ends up at the bottom of the new entries. */
function displayNewerEntries(entries) {
// for each entry
_.each(entries, function(entry) {
// remove the existing top-entry designation
$(".top-entry").removeClass("top-entry");
// create the new entry from the entry template (see ICanHas.js)
var entryElem = ich.entry(entry);
// mark the new entry as the current top-entry
entryElem.addClass("top-entry");
// push the entry on after the new-entry bar (on top of existing
// entries).
newEntryBar.after(entryElem);
// TODO: animate? If so, may need to pause after each to allow
// some animation to take place. Going for a continuous push-down
// stack effect.
});
}
/* Push the entries onto the bottom of the entry display. The function assumes
* that the entries are sorted by id, descending (2, 1, 0). Each new entry goes
* at the bottom of the stack of entries, extending the stack downwards. So the
* last entry in the list is at the very bottom of the stack. */
function displayOlderEntries(entries) {
// for each entry
_.each(entries, function(entry) {
// create the new entry from the template (ICanHas.js)
var entryElem = ich.entry(entry);
// place the entry on top of the bottom of the page
moreEntriesBar.before(entryElem);
// perhaps animate, see comments at the end of displayNewerEntries()
});
}
/* Update the user information based on the editable user-info panel. */
function updateUser(event) {
alert("TODO: update user via AJAX.");
event.preventDefault();
}
/* Show the change timeline menu. */
function showTimelineMenu(event) {
alert("TODO: show other timelines via a popup menu");
event.preventDefault();
}
/* Update the timeline details based on the editable timeline-info panel. */
function updateTimeline(event) {
var desc = $("#timeline-desc-input").val();
$.ajax({url: "/ts_api/timelines/" + user.username
+ "/" + activeTimeline.timeline_id,
type: "POST",
data: JSON.stringify({desc: desc, created: activeTimeline.created}),
error: function(jqXHR, textStatus, error) {
// TODO: better error handling
alert("Error updating timeline: \n" + jqXHR.responseText); },
success: function(data, testStatus, jqXHR) {
// TODO: check for appropriate data.status value
// update display
$("#timeline-desc").text(data.timeline.description);
}});
event.preventDefault();
}
/* Create a new entry based on the user's input in the new-entry panel. */
function newEntry(event) {
var mark = $("#new-entry-input").val();
var notes = $("#new-notes-input").val();
var timestamp = getUTCTimestamp(); // timestamp is handled on client.
// This is important. It means that the timestamps are completely in the
// hands of the user. Timestamps from this service can not be considered
// secure. This service is not a punch-card, but a time-tracker, voluntary
// and editable by the user.
var payload = JSON.stringify(
{ mark: mark, notes: notes, timestamp: timestamp });
$.ajax({url: "/ts_api/entries/" + user.username
+ "/" + activeTimeline.timeline_id,
type: "PUT",
data: payload,
error: function(jqXHR, textStatus, error) {
// TODO: better error handling.
alert("Error creating entry: \n" + jqXHR.responseText); },
success: function(data, textStatus, jqXHR) {
// TODO: check that data.status == "ok"
// grab the entry data
var newEntry = data.entry;
// push the entry on top of the stack
displayNewerEntries([newEntry]);
// clear the input
$("#new-entry-input").val("");
$("#new-notes-input").val("");
}});
event.preventDefault();
}
function toggleEditEntry(event, entryId) {
$("#entry-" + entryId + " .entry-display").toggle();
$("#entry-" + entryId + " .entry-edit").toggle();
event.preventDefault();
}
function updateEntry(event, entryId) {
var mark = $("#entry-" + entryId + "-mark-input").val();
var notes = $("#entry-" + entryId + "-notes-input").val();
var timestamp = getUTCTimestamp(); // TODO: define and read from input element
var payload = JSON.stringify(
{ mark: mark, notes: notes, timestamp: timestamp });
$.ajax({url: "/ts_api/entries/" + user.username
+ "/" + activeTimeline.timeline_id
+ "/" + entryId,
type: "POST",
data: payload,
error: function(jqXHR, textStatus, error) {
// TODO: error handling
alert("Error updating entry: \n" + jqXHR.responseText); },
success: function(data, textStatus, jqXHR) {
// TODO: check that data.status is appropriate
// update the entry display
$("#entry-" + entryId + " .entry-mark").text(data.entry.mark);
$("#entry-" + entryId + " .entry-notes").text(data.entry.notes);
}});
toggleEditEntry(event, entryId);
}
/* Delete an entry. */
function deleteEntry(event, entryId) {
$.ajax({url: "/ts_api/entries/" + user.username
+ "/" + activeTimeline.timeline_id
+ "/" + entryId,
type: "DELETE",
error: function(jqXHR, textStatus, error) {
// TODO: error handling
alert("Error updating entry: \n" + jqXHR.responseText); },
success: function(data, textStatus, jqXHR) {
$("#entry-" + entryId).slideUp('slow',
function() {$("#entry-" + entryId).remove(); });
}});
event.preventDefault();
}
/* Generate a UTC timestamp string in ISO format.
* Javascript 1.8.5 has Date.toJSON(), which is equivalent to this function,
* but is only supported in Firefox 4. */
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';
}