Implementing new client design using Backbone.js.
This commit is contained in:
27
www/js/backbone-min.js
vendored
Normal file
27
www/js/backbone-min.js
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
// Backbone.js 0.3.3
|
||||
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Backbone may be freely distributed under the MIT license.
|
||||
// For all details and documentation:
|
||||
// http://documentcloud.github.com/backbone
|
||||
(function(){var e;e=typeof exports!=="undefined"?exports:this.Backbone={};e.VERSION="0.3.3";var f=this._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var h=this.jQuery||this.Zepto;e.emulateHTTP=false;e.emulateJSON=false;e.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d<g;d++)if(b===c[d]){c.splice(d,
|
||||
1);break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,g;if(!(c=this._callbacks))return this;if(b=c[a]){d=0;for(g=b.length;d<g;d++)b[d].apply(this,Array.prototype.slice.call(arguments,1))}if(b=c.all){d=0;for(g=b.length;d<g;d++)b[d].apply(this,arguments)}return this}};e.Model=function(a,b){a||(a={});if(this.defaults)a=f.extend({},this.defaults,a);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:true});this._previousAttributes=
|
||||
f.clone(this.attributes);if(b&&b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:false,initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];return this._escapedAttributes[a]=(b==null?"":b).replace(/&(?!\w+;)/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,
|
||||
""")},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return false;if("id"in a)this.id=a.id;for(var g in a){var i=a[g];if(!f.isEqual(c[g],i)){c[g]=i;delete d[g];if(!b.silent){this._changed=true;this.trigger("change:"+g,this,i,b)}}}!b.silent&&this._changed&&this.change(b);return this},unset:function(a,b){b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&
|
||||
!this._performValidation(c,b))return false;delete this.attributes[a];delete this._escapedAttributes[a];if(!b.silent){this._changed=true;this.trigger("change:"+a,this,void 0,b);this.change(b)}return this},clear:function(a){a||(a={});var b=this.attributes,c={};for(attr in b)c[attr]=void 0;if(!a.silent&&this.validate&&!this._performValidation(c,a))return false;this.attributes={};this._escapedAttributes={};if(!a.silent){this._changed=true;for(attr in b)this.trigger("change:"+attr,this,void 0,a);this.change(a)}return this},
|
||||
fetch:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("read",this,function(d){if(!b.set(b.parse(d),a))return false;a.success&&a.success(b,d)},c);return this},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return false;var c=this,d=j(b.error,c,b),g=this.isNew()?"create":"update";(this.sync||e.sync)(g,this,function(i){if(!c.set(c.parse(i),b))return false;b.success&&b.success(c,i)},d);return this},destroy:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("delete",
|
||||
this,function(d){b.collection&&b.collection.remove(b);a.success&&a.success(b,d)},c);return this},url:function(){var a=k(this.collection);if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+this.id},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return!this.id},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=false},hasChanged:function(a){if(a)return this._previousAttributes[a]!=
|
||||
this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=false,d;for(d in a)if(!f.isEqual(b[d],a[d])){c=c||{};c[d]=a[d]}return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c){b.error?b.error(this,c):this.trigger("error",this,c,b);return false}return true}});
|
||||
e.Collection=function(a,b){b||(b={});if(b.comparator){this.comparator=b.comparator;delete b.comparator}this._boundOnModelEvent=f.bind(this._onModelEvent,this);this._reset();a&&this.refresh(a,{silent:true});this.initialize(a,b)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,
|
||||
b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){if(a==null)return null;return this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("refresh",this,a);return this},pluck:function(a){return f.map(this.models,
|
||||
function(b){return b.get(a)})},refresh:function(a,b){a||(a=[]);b||(b={});this._reset();this.add(a,{silent:true});b.silent||this.trigger("refresh",this,b);return this},fetch:function(a){a||(a={});var b=this,c=j(a.error,b,a);(this.sync||e.sync)("read",this,function(d){b.refresh(b.parse(d));a.success&&a.success(b,d)},c);return this},create:function(a,b){var c=this;b||(b={});if(a instanceof e.Model)a.collection=c;else a=new this.model(a,{collection:c});return a.save(null,{success:function(d,g){c.add(d);
|
||||
b.success&&b.success(d,g)},error:b.error})},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_add:function(a,b){b||(b={});a instanceof e.Model||(a=new this.model(a,{collection:this}));var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;a.collection=this;this.models.splice(this.comparator?this.sortedIndex(a,this.comparator):
|
||||
this.length,0,a);a.bind("all",this._boundOnModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];delete a.collection;this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);a.unbind("all",this._boundOnModelEvent);return a},_onModelEvent:function(a,b){if(a==="change:id"){delete this._byId[b.previous("id")];this._byId[b.id]=
|
||||
b}this.trigger.apply(this,arguments)}});f.each(["forEach","each","map","reduce","reduceRight","find","detect","filter","select","reject","every","all","some","any","include","invoke","max","min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty"],function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Controller=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();
|
||||
this.initialize(a)};var o=/:([\w\d]+)/g,p=/\*([\w\d]+)/g;f.extend(e.Controller.prototype,e.Events,{initialize:function(){},route:function(a,b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},saveLocation:function(a){e.history.saveLocation(a)},_bindRoutes:function(){if(this.routes)for(var a in this.routes){var b=this.routes[a];
|
||||
this.route(a,b,this[b])}},_routeToRegExp:function(a){a=a.replace(o,"([^/]*)").replace(p,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];this.fragment=this.getFragment();f.bindAll(this,"checkUrl")};var l=/^#*/;f.extend(e.History.prototype,{interval:50,getFragment:function(a){return(a||window.location).hash.replace(l,"")},start:function(){var a=document.documentMode;if(a=h.browser.msie&&(!a||a<=7))this.iframe=h('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow;
|
||||
"onhashchange"in window&&!a?h(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);return this.loadUrl()},route:function(a,b){this.handlers.push({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();if(a==this.fragment&&this.iframe)a=this.getFragment(this.iframe.location);if(a==this.fragment||a==decodeURIComponent(this.fragment))return false;if(this.iframe)window.location.hash=this.iframe.location.hash=a;this.loadUrl()},loadUrl:function(){var a=this.fragment=
|
||||
this.getFragment();return f.any(this.handlers,function(b){if(b.route.test(a)){b.callback(a);return true}})},saveLocation:function(a){a=(a||"").replace(l,"");if(this.fragment!=a){window.location.hash=this.fragment=a;if(this.iframe&&a!=this.getFragment(this.iframe.location)){this.iframe.document.open().close();this.iframe.location.hash=a}}}});e.View=function(a){this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize(a)};var q=/^(\w+)\s*(.*)$/;f.extend(e.View.prototype,e.Events,
|
||||
{tagName:"div",$:function(a){return h(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){h(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){h(this.el).unbind();for(var b in a){var c=a[b],d=b.match(q),g=d[1];d=d[2];c=f.bind(this[c],this);d===""?h(this.el).bind(g,c):h(this.el).delegate(d,g,c)}}},_configure:function(a){if(this.options)a=f.extend({},
|
||||
this.options,a);if(a.model)this.model=a.model;if(a.collection)this.collection=a.collection;if(a.el)this.el=a.el;if(a.id)this.id=a.id;if(a.className)this.className=a.className;if(a.tagName)this.tagName=a.tagName;this.options=a},_ensureElement:function(){if(!this.el){var a={};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});var m=function(a,b){var c=r(this,a,b);c.extend=m;return c};e.Model.extend=e.Collection.extend=e.Controller.extend=e.View.extend=
|
||||
m;var s={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,b,c,d){var g=s[a];a=a==="create"||a==="update"?JSON.stringify(b.toJSON()):null;b={url:k(b),type:g,contentType:"application/json",data:a,dataType:"json",processData:false,success:c,error:d};if(e.emulateJSON){b.contentType="application/x-www-form-urlencoded";b.processData=true;b.data=a?{model:a}:{}}if(e.emulateHTTP)if(g==="PUT"||g==="DELETE"){if(e.emulateJSON)b.data._method=g;b.type="POST";b.beforeSend=function(i){i.setRequestHeader("X-HTTP-Method-Override",
|
||||
g)}}h.ajax(b)};var n=function(){},r=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};n.prototype=a.prototype;d.prototype=new n;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){if(!(a&&a.url))throw Error("A 'url' property or function must be specified");return f.isFunction(a.url)?a.url():a.url},j=function(a,b,c){return function(d){a?a(b,d):b.trigger("error",b,d,c)}}})();
|
1098
www/js/backbone.js
Normal file
1098
www/js/backbone.js
Normal file
File diff suppressed because it is too large
Load Diff
69
www/js/test.js
Normal file
69
www/js/test.js
Normal file
@ -0,0 +1,69 @@
|
||||
var Test = {};
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
Test.U = Backbone.Model.extend({
|
||||
url: '/ts_api/users'
|
||||
});
|
||||
|
||||
Test.UView = Backbone.View.extend({
|
||||
|
||||
el: $("#user"),
|
||||
model: Test.U,
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.model.bind('change', this.render);
|
||||
this.model.view = this;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var user = this.model.get('user');
|
||||
this.$('.fullname').text(user.name);
|
||||
this.$('.username').text(user.username);
|
||||
}
|
||||
});
|
||||
|
||||
Test.currentUser = new Test.U;
|
||||
Test.userView = new Test.UView({model: Test.currentUser});
|
||||
|
||||
// wire the login dialog using jQuery UI
|
||||
$("#login-dialog").dialog({
|
||||
autoOpen: false,
|
||||
height: 400,
|
||||
width: 400,
|
||||
modal: true,
|
||||
buttons: { Login: function(){login()} }
|
||||
});
|
||||
|
||||
$('#login-dialog').dialog('open');
|
||||
|
||||
});
|
||||
|
||||
function login() {
|
||||
// lookup the login dialog elements
|
||||
var name = $("#login-name");
|
||||
var pwd = $("#login-password");
|
||||
|
||||
// call the API via AJAX
|
||||
$.ajax({
|
||||
url: "/ts_api/login",
|
||||
processData: false,
|
||||
data: JSON.stringify({username: name.val(), password: pwd.val()}),
|
||||
type: "POST",
|
||||
|
||||
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.addClass("ui-state-error");
|
||||
tips.slideDown();
|
||||
},
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
$("#login-dialog").dialog('close');
|
||||
Test.currentUser.set({id: name.val()}, {silent: true});
|
||||
Test.currentUser.fetch();
|
||||
}});
|
||||
}
|
542
www/js/ts.js
542
www/js/ts.js
@ -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';
|
||||
}
|
||||
|
Reference in New Issue
Block a user