Bugfixes: session management, new entry creation.
* 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.
This commit is contained in:
parent
0278179452
commit
a3c55e918e
@ -190,7 +190,7 @@ do_login(YArg) ->
|
||||
{CookieVal, _Session} = ts_api_session:new(Username),
|
||||
|
||||
[{header, {set_cookie, io_lib:format(
|
||||
"ts_api_session=~s; Path=/ts_api",
|
||||
"ts_api_session=~s; Path=/",
|
||||
[CookieVal])}},
|
||||
{header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
|
||||
{header, ["Access-Control-Allow-Credentials: ", "true"]},
|
||||
@ -269,7 +269,7 @@ put_user(YArg, Username) ->
|
||||
throw(make_json_400(YArg, [{request_error, InputError}]))
|
||||
end,
|
||||
|
||||
% update the record (we do not support creating users via the API right now
|
||||
% update the record (we do not support creating users via the API right now)
|
||||
{ok, UpdatedRec} = ts_user:update(UR, ExtData),
|
||||
|
||||
% return a 200
|
||||
|
@ -81,31 +81,31 @@ ejson_to_record(Empty, {struct, EJSONFields}) ->
|
||||
construct_record(Empty, EJSONFields, []).
|
||||
|
||||
ejson_to_record(Empty, Ref, EJSON) ->
|
||||
Constructed = ejson_to_record(Empty, EJSON),
|
||||
setelement(2, Constructed, Ref).
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
{setelement(2, Constructed, Ref), ExtData}.
|
||||
|
||||
ejson_to_record_strict(Empty=#ts_user{}, EJSON) ->
|
||||
Constructed = ejson_to_record(Empty, EJSON),
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
case Constructed of
|
||||
#ts_user{name = undefined} -> throw("Missing user 'name' field.");
|
||||
#ts_user{email = undefined} -> throw("Missing user 'email' field.");
|
||||
#ts_user{join_date = undefined} ->
|
||||
throw("Missing user 'join_date' field.");
|
||||
_Other -> Constructed
|
||||
_Other -> {Constructed, ExtData}
|
||||
end;
|
||||
|
||||
ejson_to_record_strict(Empty=#ts_timeline{}, EJSON) ->
|
||||
Constructed = ejson_to_record(Empty, EJSON),
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
case Constructed of
|
||||
#ts_timeline{created = undefined} ->
|
||||
throw("Missing timeline 'created' field.");
|
||||
#ts_timeline{desc = undefined} ->
|
||||
throw("Missing timeline 'description' field.");
|
||||
_Other -> Constructed
|
||||
_Other -> {Constructed, ExtData}
|
||||
end;
|
||||
|
||||
ejson_to_record_strict(Empty=#ts_entry{}, EJSON) ->
|
||||
Constructed = ejson_to_record(Empty, EJSON),
|
||||
{Constructed, ExtData} = ejson_to_record(Empty, EJSON),
|
||||
case Constructed of
|
||||
#ts_entry{timestamp = undefined} ->
|
||||
throw("Missing timelne 'timestamp' field.");
|
||||
@ -113,12 +113,12 @@ ejson_to_record_strict(Empty=#ts_entry{}, EJSON) ->
|
||||
throw("Missing timeline 'mark' field.");
|
||||
#ts_entry{notes = undefined} ->
|
||||
throw("Missing timeline 'notes' field/");
|
||||
_Other -> Constructed
|
||||
_Other -> {Constructed, ExtData}
|
||||
end.
|
||||
|
||||
ejson_to_record_strict(Empty, Ref, EJSON) ->
|
||||
Constructed = ejson_to_record_strict(Empty, EJSON),
|
||||
setelement(2, Constructed, Ref).
|
||||
{Constructed, ExtData} = ejson_to_record_strict(Empty, EJSON),
|
||||
{setelement(2, Constructed, Ref), ExtData}.
|
||||
|
||||
construct_record(User=#ts_user{}, [{Key, Value}|Fields], ExtData) ->
|
||||
case Key of
|
||||
|
@ -28,33 +28,38 @@ out(YArg) ->
|
||||
Username = element(2, Session),
|
||||
|
||||
% get the user
|
||||
{content, _, UserJSON} = ts_api:get_user(YArg, Username),
|
||||
UserResp = ts_api:get_user(YArg, Username),
|
||||
{content, _, UserJSON} = lists:keyfind(content, 1, UserResp),
|
||||
|
||||
UserRecord = ts_user:lookup(Username),
|
||||
|
||||
% get the timelines
|
||||
{content, _, TimelineListJSON} = ts_api:list_timelines(YArg, Username),
|
||||
TimelineResp = ts_api:list_timelines(YArg, Username),
|
||||
{content, _, TimelineListJSON} = lists:keyfind(content, 1, TimelineResp),
|
||||
|
||||
% get the selected timeline
|
||||
SelectedTimeline = case lists:keyfind(
|
||||
selected_timeline, 1, element(8, UserRecord)) of
|
||||
false -> ts_timeline:list(Username, 0, 1);
|
||||
% get the last used timeline if there is one.
|
||||
SelectedTimeline = case ts_ext_data:get_property(Username, last_timeline) of
|
||||
|
||||
not_set -> ts_timeline:list(Username, 0, 1);
|
||||
T -> T
|
||||
end,
|
||||
|
||||
% get entries for this timeline
|
||||
{content, _, EntryListJSON} =
|
||||
EntriesResp =
|
||||
ts_api:list_entries(YArg, Username, SelectedTimeline),
|
||||
{content, _, EntryListJSON} = lists:keyfind(content, 1, EntriesResp),
|
||||
|
||||
{html, f(
|
||||
"function bootstrap() {~n"
|
||||
" var data = {};~n"
|
||||
" data.user = ~p;~n"
|
||||
" data.timelines = ~p;~n"
|
||||
" data.user = ~s;~n"
|
||||
" data.timelines = ~s;~n"
|
||||
" data.initialTimelineId = ~p;~n"
|
||||
" data.entries = ~p;~n"
|
||||
" data.entries = ~s;~n"
|
||||
" return data;~n"
|
||||
"};",
|
||||
[UserJSON, TimelineListJSON, SelectedTimeline, EntryListJSON])}
|
||||
[lists:flatten(UserJSON), lists:flatten(TimelineListJSON),
|
||||
SelectedTimeline, lists:flatten(EntryListJSON)])}
|
||||
end.
|
||||
</erl>
|
||||
</script>
|
||||
|
309
www/js/ts.js
309
www/js/ts.js
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* # `ts.js`: TimeStamper Web UI Client-side
|
||||
* @author Jonathan Bernard <jdb@jdb-labs.com>
|
||||
**/
|
||||
|
||||
// TimeStamper namespace
|
||||
var TS = {};
|
||||
|
||||
@ -8,24 +13,27 @@ $(document).ready(function(){
|
||||
|
||||
/* 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);
|
||||
}
|
||||
},
|
||||
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 = {}
|
||||
@ -33,44 +41,42 @@ $(document).ready(function(){
|
||||
if (key == "timestamp") {
|
||||
if (val instanceof Date) {
|
||||
this.timestampDate = val;
|
||||
attrsToSet.timestamp = dateToJSON(val);
|
||||
} else {
|
||||
attrsToSet.timestamp = dateToJSON(val); }
|
||||
else {
|
||||
this.timestampDate = new Date(val);
|
||||
attrsToSet.timestamp = dateToJSON(this.timestampDate);
|
||||
}
|
||||
} else {
|
||||
attrsToSet[key] = val;
|
||||
}
|
||||
});
|
||||
attrsToSet.timestamp = dateToJSON(this.timestampDate); } }
|
||||
else { attrsToSet[key] = val; } });
|
||||
|
||||
return Backbone.Model.prototype.set.call(this, attrsToSet, options);
|
||||
}
|
||||
});
|
||||
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:
|
||||
* - username
|
||||
* - fullname
|
||||
* - id
|
||||
* - name
|
||||
* - email
|
||||
* - join_date
|
||||
*/
|
||||
TS.UserModel = Backbone.Model.extend({
|
||||
url: function() { return '/ts_api/users/' + this.get('id'); },
|
||||
|
||||
initialize: function(attrs, options) {
|
||||
_.bind(this, 'url');
|
||||
}
|
||||
});
|
||||
initialize: function(attrs, options) { _.bindAll(this, 'url'); } });
|
||||
|
||||
TS.EntryList = Backbone.Collection.extend({
|
||||
model: TS.EntryModel,
|
||||
@ -79,17 +85,23 @@ $(document).ready(function(){
|
||||
|
||||
initialize: function(model, options) {
|
||||
if (options.timelineModel == undefined) {
|
||||
throw "Cannot create an EntryList without a TimelineModel reference."
|
||||
} else { this.timelineModel = options.timelineModel; }
|
||||
throw "Cannot create an EntryList without a " +
|
||||
"TimelineModel reference." }
|
||||
else { this.timelineModel = options.timelineModel; }
|
||||
|
||||
_.bindAll(this, "url");
|
||||
},
|
||||
_.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');
|
||||
}
|
||||
});
|
||||
+ this.timelineModel.get('id'); } });
|
||||
|
||||
TS.TimelineList = Backbone.Collection.extend({
|
||||
model: TS.TimelineModel,
|
||||
@ -99,17 +111,12 @@ $(document).ready(function(){
|
||||
throw "Cannot create a TimelineList without a UserModel reference.";
|
||||
} else { this.user = options.user; }
|
||||
|
||||
_.bindAll(this, 'url');
|
||||
},
|
||||
_.bindAll(this, 'url'); },
|
||||
|
||||
comparator: function(timeline) {
|
||||
return timeline.get('id');
|
||||
},
|
||||
comparator: function(timeline) { return timeline.get('id'); },
|
||||
|
||||
url: function() {
|
||||
return "/ts_api/timelines/" + this.user.get('id');
|
||||
}
|
||||
});
|
||||
return "/ts_api/timelines/" + this.user.get('id'); } });
|
||||
|
||||
|
||||
// ======== DEFINE VIEWS ========//
|
||||
@ -135,8 +142,7 @@ $(document).ready(function(){
|
||||
"keypress .notes-input" : "saveOnCtrlEnter",
|
||||
"blur .mark-input" : "save",
|
||||
"blur .timestamp-input" : "save",
|
||||
"blur .notes-input" : "save"
|
||||
},
|
||||
"blur .notes-input" : "save" },
|
||||
|
||||
initialize: function(options) {
|
||||
_.bindAll(this, 'render', 'editTImestamp', 'editMark', 'update',
|
||||
@ -148,8 +154,7 @@ $(document).ready(function(){
|
||||
this.model.bind('change', this.update);
|
||||
this.model.view = this;
|
||||
|
||||
this.nextModel = options.nextModel;
|
||||
},
|
||||
this.nextModel = options.nextModel; },
|
||||
|
||||
/**
|
||||
* Refresh the display based on the model replacing the existing
|
||||
@ -162,8 +167,7 @@ $(document).ready(function(){
|
||||
// invalidate the notes display cache
|
||||
this.notesCache = false;
|
||||
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
/**
|
||||
* Refresh the display based on the model using the existing DOM
|
||||
@ -178,27 +182,23 @@ $(document).ready(function(){
|
||||
this.$('.duration').text(data.duration);
|
||||
this.$('.notes-text').html(this.renderNotes(data.notes));
|
||||
this.$('.notes-input').val(data.notes);
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
renderNotes: function(source) {
|
||||
if (!this.notesCache) {
|
||||
this.notesCache = this.markdownConverter.makeHtml(source); }
|
||||
|
||||
return this.notesCache
|
||||
},
|
||||
return this.notesCache },
|
||||
|
||||
editMark: function() {
|
||||
$(this.el).addClass('edit-mark');
|
||||
this.$('.mark-input').focus();
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
editTimestamp: function() {
|
||||
$(this.el).addClass('edit-timestamp');
|
||||
this.$('timestamp-input').focus();
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
editNotes: function() {
|
||||
// invalidate notes HTML cache
|
||||
@ -209,8 +209,7 @@ $(document).ready(function(){
|
||||
|
||||
// focus input
|
||||
this.$('.notes-input').focus();
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
/**
|
||||
* Translate the model data into a form suitable to be displayed.
|
||||
@ -224,8 +223,7 @@ $(document).ready(function(){
|
||||
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;
|
||||
},
|
||||
return data; },
|
||||
|
||||
/** Save and close editable fields. */
|
||||
save: function() {
|
||||
@ -235,17 +233,12 @@ $(document).ready(function(){
|
||||
notes: this.$('.notes-input').val()});
|
||||
|
||||
this.$('.notes-text').html(this.renderNotes(this.model.get('notes')));
|
||||
$(this.el).removeClass('edit-mark edit-timestamp edit-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(); }
|
||||
},
|
||||
saveOnEnter: function(e) { if(e.keyCode == 13) { this.save(); } },
|
||||
|
||||
saveOnCtrlEnter: function(e) {
|
||||
if (e.keyCode == 10) { this.save(); }
|
||||
},
|
||||
saveOnCtrlEnter: function(e) { if (e.keyCode == 10) { this.save(); } },
|
||||
|
||||
/**
|
||||
* Get the display-able start time from the entry timestamp.
|
||||
@ -263,8 +256,7 @@ $(document).ready(function(){
|
||||
// 12 hour
|
||||
var hour = startDate.getHours() % 12;
|
||||
return (hour == 0 ? 12 : hour) + ":" + startDate.getMinutes() +
|
||||
" " + (startDate.getHours() > 11 ? "pm" : "am");
|
||||
},
|
||||
" " + (startDate.getHours() > 11 ? "pm" : "am"); },
|
||||
|
||||
/**
|
||||
* Get the duration of the entry based on this entry's timestamp and
|
||||
@ -296,8 +288,7 @@ $(document).ready(function(){
|
||||
|
||||
return (day > 0 ? day + "d " : "") +
|
||||
(hr > 0 ? hr + "hr " : "") +
|
||||
min + "m ";
|
||||
},
|
||||
min + "m "; },
|
||||
|
||||
showNotes: function() {
|
||||
if (!this.notesCache) {
|
||||
@ -305,22 +296,17 @@ $(document).ready(function(){
|
||||
this.renderNotes(this.model.get('notes'))); }
|
||||
|
||||
this.$('.notes').slideDown();
|
||||
$(this.el).addClass('show-notes');
|
||||
},
|
||||
$(this.el).addClass('show-notes'); },
|
||||
|
||||
hideNotes: function() {
|
||||
this.$('.notes').slideUp();
|
||||
$(this.el).removeClass('show-notes');
|
||||
}
|
||||
});
|
||||
$(this.el).removeClass('show-notes'); } });
|
||||
|
||||
TS.EntryListView = Backbone.View.extend({
|
||||
|
||||
el: $("#entry-list"),
|
||||
|
||||
events: {
|
||||
"keypress #new-entry-input" : "createNewEntryOnEnter"
|
||||
},
|
||||
events: { "keypress #new-entry-input" : "createNewEntryOnEnter" },
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'addOne', 'createNewEntry', 'render', 'renderOne');
|
||||
@ -328,15 +314,13 @@ $(document).ready(function(){
|
||||
this.collection.bind('refresh', this.render);
|
||||
this.collection.view = this;
|
||||
this.entryContainer = this.$("#entries")
|
||||
this.markdownConverter = new Showdown.converter();
|
||||
},
|
||||
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);
|
||||
},
|
||||
this.renderOne(entry, null); },
|
||||
|
||||
renderOne: function(entry, nextEntry) {
|
||||
// exclude if any exclusion RegExps match
|
||||
@ -357,9 +341,7 @@ $(document).ready(function(){
|
||||
// hide it if excluded
|
||||
if (excluded) {
|
||||
$(el).fadeOut('slow');
|
||||
$(el).addClass('excluded');
|
||||
}
|
||||
},
|
||||
$(el).addClass('excluded'); } },
|
||||
|
||||
createNewEntryOnEnter: function(e) {
|
||||
|
||||
@ -371,13 +353,12 @@ $(document).ready(function(){
|
||||
// create the mark. Immediately fetch to get server-side timestamp
|
||||
this.collection.create({mark: entryMark,
|
||||
notes: '',
|
||||
timestamp: new Date()}).fetch();
|
||||
timestamp: new Date()});
|
||||
|
||||
this.collection.fetch();
|
||||
|
||||
// clear the input for the next entry
|
||||
this.$("#new-entry-input").val("");
|
||||
|
||||
}
|
||||
},
|
||||
this.$("#new-entry-input").val(""); } },
|
||||
|
||||
render: function() {
|
||||
|
||||
@ -416,12 +397,9 @@ $(document).ready(function(){
|
||||
if (currentDay.getDate() != entry.get("timestamp").getDate()) {
|
||||
this.topSeparator.after(ich.daySeparatorTemplate(
|
||||
{separatorLabel: this.formatDaySeparator(today, currentDay)}));
|
||||
currentDay = entry.get('timestamp');
|
||||
}
|
||||
currentDay = entry.get('timestamp'); }
|
||||
|
||||
this.renderOne(entry, nextEntry);
|
||||
}
|
||||
},
|
||||
this.renderOne(entry, nextEntry); } },
|
||||
|
||||
formatDaySeparator: function(today, labelDay) {
|
||||
|
||||
@ -456,10 +434,7 @@ $(document).ready(function(){
|
||||
else if (dayDiff == 1) { return "Yesterday"; }
|
||||
|
||||
// today
|
||||
else if (dayDiff == 0) { return "Today"; }
|
||||
|
||||
}
|
||||
});
|
||||
else if (dayDiff == 0) { return "Today"; } } });
|
||||
|
||||
TS.TimelineListView = Backbone.View.extend({
|
||||
el: $("#timeline"),
|
||||
@ -471,65 +446,53 @@ $(document).ready(function(){
|
||||
"dblclick .timeline-desc" : "editDesc",
|
||||
"keypress .timeline-id-input" : "saveOnEnter",
|
||||
"keypress .timeline-desc-input" : "saveOnEnter",
|
||||
"click .new-timeline-link" : "showNewTimelineDialog"
|
||||
},
|
||||
"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);
|
||||
}
|
||||
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);
|
||||
},
|
||||
this.collection.bind('refresh', this.render); },
|
||||
|
||||
renderOne: function(timeline) {
|
||||
this.$('.drop-menu-items').append(
|
||||
ich.timelineLinkTemplate(timeline.toJSON()));
|
||||
},
|
||||
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);
|
||||
},
|
||||
_.each(this.collection.without([this.selectedModel]), this.renderOne); },
|
||||
|
||||
editId: function() {
|
||||
$(this.el).addClass('edit-id');
|
||||
this.$('.timeline-id-input').focus();
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
editDesc: function() {
|
||||
$(this.el).addClass('edit-desc');
|
||||
this.$('.timeline-desc-input').focus();
|
||||
return this;
|
||||
},
|
||||
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();
|
||||
},
|
||||
this.render(); },
|
||||
|
||||
showNewTimelineDialog: function() {
|
||||
TS.app.newTimelineDialog.show();
|
||||
},
|
||||
showNewTimelineDialog: function() { TS.app.newTimelineDialog.show(); },
|
||||
|
||||
saveOnEnter: function(e) {
|
||||
if (e.keyCode == 13) { this.save(); }
|
||||
}
|
||||
|
||||
});
|
||||
saveOnEnter: function(e) { if (e.keyCode == 13) { this.save(); } } });
|
||||
|
||||
TS.UserView = Backbone.View.extend({
|
||||
|
||||
@ -539,44 +502,34 @@ $(document).ready(function(){
|
||||
|
||||
events: {
|
||||
'dblclick .fullname': 'editFullname',
|
||||
'keypress .fullname-input': 'saveOnEnter'
|
||||
},
|
||||
'keypress .fullname-input': 'saveOnEnter' },
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render', 'save', 'editFullname', 'saveOnEnter');
|
||||
this.model.bind('change', this.render);
|
||||
this.model.view = this;
|
||||
},
|
||||
this.model.view = this; },
|
||||
|
||||
render: function() {
|
||||
$(this.el).html(ich.userTemplate(this.model.toJSON()));
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
editFullname: function() {
|
||||
$(this.el).addClass('edit-fullname');
|
||||
this.$('.fullname-input').focus();
|
||||
return this;
|
||||
},
|
||||
return this; },
|
||||
|
||||
save: function() {
|
||||
this.model.set({name: this.$('fullname-input').val()});
|
||||
this.model.save();
|
||||
$(this.el).removeClass('edit-fullname');
|
||||
},
|
||||
$(this.el).removeClass('edit-fullname'); },
|
||||
|
||||
saveOnEnter: function(e) {
|
||||
if (e.keyCode == 13) this.save();
|
||||
}
|
||||
});
|
||||
saveOnEnter: function(e) { if (e.keyCode == 13) this.save(); } });
|
||||
|
||||
TS.AppView = Backbone.View.extend({
|
||||
|
||||
el: $("body"),
|
||||
|
||||
events: {
|
||||
'click #timeline .drop-menu-items a': 'selectTimeline'
|
||||
},
|
||||
events: { 'click #timeline .drop-menu-items a': 'selectTimeline' },
|
||||
|
||||
initialize: function() {
|
||||
|
||||
@ -593,12 +546,12 @@ $(document).ready(function(){
|
||||
else {
|
||||
// this is async (waiting for user input)
|
||||
this.loginDialog.authenticate(function() {
|
||||
appThis.initializeData(appThis.loadInitialData())});
|
||||
}
|
||||
},
|
||||
appThis.initializeData(appThis.loadInitialData())}); } },
|
||||
|
||||
initializeData: function(data) {
|
||||
|
||||
TS.app = this;
|
||||
|
||||
// create user data
|
||||
this.user = {};
|
||||
this.user.model = new TS.UserModel(data.user);
|
||||
@ -606,8 +559,7 @@ $(document).ready(function(){
|
||||
|
||||
// create timeline models from the bootstrapped data
|
||||
var tlModels = _.map(data.timelines, function(timeline) {
|
||||
return new TS.TimelineModel(timeline);
|
||||
});
|
||||
return new TS.TimelineModel(timeline); });
|
||||
|
||||
// create the timeline list collection
|
||||
this.timelines = {};
|
||||
@ -623,8 +575,7 @@ $(document).ready(function(){
|
||||
|
||||
// create entry models from the bootstrapped data
|
||||
var entryModels = _.map(data.entries, function(entry) {
|
||||
return new TS.EntryModel(entry);
|
||||
});
|
||||
return new TS.EntryModel(entry); });
|
||||
|
||||
// create the entry collection
|
||||
this.entries = {};
|
||||
@ -636,10 +587,7 @@ $(document).ready(function(){
|
||||
// render views
|
||||
this.user.view.render();
|
||||
this.timelines.view.render();
|
||||
this.entries.view.render();
|
||||
|
||||
|
||||
},
|
||||
this.entries.view.render(); },
|
||||
|
||||
loadInitialData: function() {
|
||||
// assume we are authenticated
|
||||
@ -658,8 +606,7 @@ $(document).ready(function(){
|
||||
data.initialTimelineId,
|
||||
async: false}).responseText);
|
||||
|
||||
return data;
|
||||
},
|
||||
return data; },
|
||||
|
||||
selectTimeline: function(e) {
|
||||
if (e) {
|
||||
@ -680,32 +627,24 @@ $(document).ready(function(){
|
||||
this.timelines.view.render();
|
||||
|
||||
// refresh EntryList records
|
||||
this.entries.collection.fetch()
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
this.entries.collection.fetch() } } });
|
||||
|
||||
TS.LoginView = Backbone.View.extend({
|
||||
el: $("#login"),
|
||||
|
||||
events: {
|
||||
"keypress #password-input" : "loginOnEnter",
|
||||
"click #login-button a" : "doLogin"
|
||||
},
|
||||
"click #login-button a" : "doLogin" },
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'authenticate', 'doLogin', 'hide', 'loginOnEnter',
|
||||
'show');
|
||||
|
||||
},
|
||||
'show'); },
|
||||
|
||||
action: function() {},
|
||||
|
||||
authenticate: function(nextAction) {
|
||||
this.action = nextAction;
|
||||
this.show();
|
||||
},
|
||||
this.show(); },
|
||||
|
||||
doLogin: function(){
|
||||
var viewThis = this;
|
||||
@ -725,46 +664,35 @@ $(document).ready(function(){
|
||||
// we should check that, FIXME
|
||||
var tips = $(".validate-tips");
|
||||
tips.text("Incorrect username/password combination.");
|
||||
tips.slideDown();
|
||||
},
|
||||
tips.slideDown(); },
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
viewThis.hide();
|
||||
viewThis.action();
|
||||
}
|
||||
});
|
||||
},
|
||||
viewThis.action(); } }); },
|
||||
|
||||
hide: function() { $(this.el).addClass('hidden'); },
|
||||
|
||||
show: function() {
|
||||
$(this.el).removeClass('hidden');
|
||||
this.$("#username-input").focus();
|
||||
},
|
||||
this.$("#username-input").focus(); },
|
||||
|
||||
loginOnEnter: function(e) {
|
||||
if (e.keyCode == 13) { this.doLogin(); }
|
||||
}
|
||||
});
|
||||
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"
|
||||
},
|
||||
"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;
|
||||
}
|
||||
|
||||
},
|
||||
throw "Can not create the NewTimelineView without the " +
|
||||
"timeline collection." }
|
||||
else { this.timelineCollection = options.timelineCollection; } },
|
||||
|
||||
createTimeline: function() {
|
||||
var timelineId = this.$("#new-timeline-id").val();
|
||||
@ -772,8 +700,7 @@ $(document).ready(function(){
|
||||
this.timelineCollection.create(
|
||||
{id: timelineId, description: timelineDesc,
|
||||
created: dateToJSON(new Date())});
|
||||
this.hide();
|
||||
},
|
||||
this.hide(); },
|
||||
|
||||
hide: function() { $(this.el).addClass('hidden'); },
|
||||
|
||||
@ -781,11 +708,9 @@ $(document).ready(function(){
|
||||
this.$("#new-timeline-id").val("");
|
||||
this.$("#new-timeline-desc").val("");
|
||||
$(this.el).removeClass('hidden');
|
||||
this.$("#new-timeline-id").focus();
|
||||
}
|
||||
});
|
||||
this.$("#new-timeline-id").focus(); } });
|
||||
|
||||
TS.app = new TS.AppView;
|
||||
new TS.AppView();
|
||||
|
||||
})
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user