Check if success flag is true before accessing values.
[Sone.git] / src / main / resources / static / javascript / sone.js
1 /* Sone JavaScript functions. */
2
3 /* jQuery overrides. */
4 oldGetJson = jQuery.prototype.getJSON;
5 jQuery.prototype.getJSON = function(url, data, successCallback, errorCallback) {
6         if (typeof errorCallback == "undefined") {
7                 return oldGetJson(url, data, successCallback);
8         }
9         if (jQuery.isFunction(data)) {
10                 errorCallback = successCallback;
11                 successCallback = data;
12                 data = null;
13         }
14         return jQuery.ajax({
15                 data: data,
16                 error: errorCallback,
17                 success: successCallback,
18                 url: url
19         });
20 }
21
22 function isOnline() {
23         return $("#sone").hasClass("online");
24 }
25
26 function registerInputTextareaSwap(inputSelector, defaultText, inputFieldName, optional, dontUseTextarea) {
27         $(inputSelector).each(function() {
28                 textarea = $(dontUseTextarea ? "<input type=\"text\" name=\"" + inputFieldName + "\">" : "<textarea name=\"" + inputFieldName + "\"></textarea>").blur(function() {
29                         if ($(this).val() == "") {
30                                 $(this).hide();
31                                 inputField = $(this).data("inputField");
32                                 inputField.show().removeAttr("disabled").addClass("default");
33                                 inputField.val(defaultText);
34                         }
35                 }).hide().data("inputField", $(this)).val($(this).val());
36                 $(this).after(textarea);
37                 (function(inputField, textarea) {
38                         inputField.focus(function() {
39                                 $(this).hide().attr("disabled", "disabled");
40                                 textarea.show().focus();
41                         });
42                         if (inputField.val() == "") {
43                                 inputField.addClass("default");
44                                 inputField.val(defaultText);
45                         } else {
46                                 inputField.hide().attr("disabled", "disabled");
47                                 textarea.show();
48                         }
49                         $(inputField.get(0).form).submit(function() {
50                                 if (!optional && (textarea.val() == "")) {
51                                         return false;
52                                 }
53                         });
54                 })($(this), textarea);
55         });
56 }
57
58 /* hide all the “create reply” forms until a link is clicked. */
59 function addCommentLinks() {
60         if (!isOnline()) {
61                 return;
62         }
63         $("#sone .post").each(function() {
64                 postId = $(this).attr("id");
65                 addCommentLink(postId, $(this));
66         });
67 }
68
69 /**
70  * Adds a “comment” link to all status lines contained in the given element.
71  *
72  * @param postId
73  *            The ID of the post
74  * @param element
75  *            The element to add a “comment” link to
76  */
77 function addCommentLink(postId, element) {
78         commentElement = (function(postId) {
79                 var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
80                         replyElement = $("#sone .post#" + postId + " .create-reply");
81                         replyElement.removeClass("hidden");
82                         replyElement.removeClass("light");
83                         (function(replyElement) {
84                                 replyElement.find("input.reply-input").blur(function() {
85                                         if ($(this).hasClass("default")) {
86                                                 replyElement.addClass("light");
87                                         }
88                                 }).focus(function() {
89                                         replyElement.removeClass("light");
90                                 });
91                         })(replyElement);
92                         replyElement.find("input.reply-input").focus();
93                 });
94                 return commentElement;
95         })(postId);
96         element.find(".create-reply").addClass("hidden");
97         element.find(".status-line .time").each(function() {
98                 $(this).after(commentElement.clone(true));
99         });
100 }
101
102 /**
103  * Retrieves the translation for the given key and calls the callback function.
104  * The callback function takes a single parameter, the translated string.
105  *
106  * @param key
107  *            The key of the translation string
108  * @param callback
109  *            The callback function
110  */
111 function getTranslation(key, callback) {
112         $.getJSON("ajax/getTranslation.ajax", {"key": key}, function(data, textStatus) {
113                 if ((data != null) && data.success) {
114                         callback(data.value);
115                 }
116         }, function(xmlHttpRequest, textStatus, error) {
117                 /* ignore error. */
118         });
119 }
120
121 /**
122  * Fires off an AJAX request to retrieve the current status of a Sone.
123  *
124  * @param soneId
125  *            The ID of the Sone
126  * @param local
127  *            <code>true</code> if the Sone is local, <code>false</code>
128  *            otherwise
129  */
130 function getSoneStatus(soneId, local) {
131         $.getJSON("ajax/getSoneStatus.ajax", {"sone": soneId}, function(data, textStatus) {
132                 if ((data != null) && data.success) {
133                         updateSoneStatus(soneId, data.name, data.status, data.modified, data.lastUpdated);
134                 }
135                 /* seconds! */
136                 updateInterval = 60;
137                 if (local || (data!= null) && (data.modified || (data.status == "downloading") || (data.status == "inserting"))) {
138                         updateInterval = 5;
139                 }
140                 setTimeout(function() {
141                         getSoneStatus(soneId, local);
142                 }, updateInterval * 1000);
143         }, function(xmlHttpRequest, textStatus, error) {
144                 /* ignore error. */
145         });
146 }
147
148 /**
149  * Filters the given Sone ID, replacing all “~” characters by an underscore.
150  *
151  * @param soneId
152  *            The Sone ID to filter
153  * @returns The filtered Sone ID
154  */
155 function filterSoneId(soneId) {
156         return soneId.replace(/[^a-zA-Z0-9-]/g, "_");
157 }
158
159 /**
160  * Updates the status of the given Sone.
161  *
162  * @param soneId
163  *            The ID of the Sone to update
164  * @param status
165  *            The status of the Sone (“idle”, “unknown”, “inserting”,
166  *            “downloading”)
167  * @param modified
168  *            Whether the Sone is modified
169  * @param lastUpdated
170  *            The date and time of the last update (formatted for display)
171  */
172 function updateSoneStatus(soneId, name, status, modified, lastUpdated) {
173         $("#sone .sone." + filterSoneId(soneId)).
174                 toggleClass("unknown", status == "unknown").
175                 toggleClass("idle", status == "idle").
176                 toggleClass("inserting", status == "inserting").
177                 toggleClass("downloading", status == "downloading").
178                 toggleClass("modified", modified);
179         $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
180         $("#sone .sone." + filterSoneId(soneId) + " .profile-link a").text(name);
181 }
182
183 var watchedSones = {};
184
185 /**
186  * Watches this Sone for updates to its status.
187  *
188  * @param soneId
189  *            The ID of the Sone to watch
190  * @param local
191  *            <code>true</code> if the Sone is local, <code>false</code>
192  *            otherwise
193  */
194 function watchSone(soneId, local) {
195         if (watchedSones[soneId]) {
196                 return;
197         }
198         watchedSones[soneId] = true;
199         (function(soneId) {
200                 setTimeout(function() {
201                         getSoneStatus(soneId, local);
202                 }, 5000);
203         })(soneId);
204 }
205
206 /**
207  * Enhances a “delete” button so that the confirmation is done on the same page.
208  *
209  * @param buttonId
210  *            The selector of the button
211  * @param text
212  *            The text to show on the button
213  * @param deleteCallback
214  *            The callback that actually deletes something
215  */
216 function enhanceDeleteButton(buttonId, text, deleteCallback) {
217         button = $(buttonId);
218         (function(button) {
219                 newButton = $("<button></button>").addClass("confirm").hide().text(text).click(function() {
220                         $(this).fadeOut("slow");
221                         deleteCallback();
222                         return false;
223                 }).insertAfter(button);
224                 (function(button, newButton) {
225                         button.click(function() {
226                                 button.fadeOut("slow", function() {
227                                         newButton.fadeIn("slow");
228                                         $(document).one("click", function() {
229                                                 if (this != newButton.get(0)) {
230                                                         newButton.fadeOut(function() {
231                                                                 button.fadeIn();
232                                                         });
233                                                 }
234                                         });
235                                 });
236                                 return false;
237                         });
238                 })(button, newButton);
239         })(button);
240 }
241
242 /**
243  * Enhances a post’s “delete” button.
244  *
245  * @param buttonId
246  *            The selector of the button
247  * @param postId
248  *            The ID of the post to delete
249  * @param text
250  *            The text to replace the button with
251  */
252 function enhanceDeletePostButton(buttonId, postId, text) {
253         enhanceDeleteButton(buttonId, text, function() {
254                 $.getJSON("ajax/deletePost.ajax", { "post": postId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
255                         if (data == null) {
256                                 return;
257                         }
258                         if (data.success) {
259                                 $("#sone .post#" + postId).slideUp();
260                         } else if (data.error == "invalid-post-id") {
261                                 alert("Invalid post ID given!");
262                         } else if (data.error == "auth-required") {
263                                 alert("You need to be logged in.");
264                         } else if (data.error == "not-authorized") {
265                                 alert("You are not allowed to delete this post.");
266                         }
267                 }, function(xmlHttpRequest, textStatus, error) {
268                         /* ignore error. */
269                 });
270         });
271 }
272
273 /**
274  * Enhances a reply’s “delete” button.
275  *
276  * @param buttonId
277  *            The selector of the button
278  * @param replyId
279  *            The ID of the reply to delete
280  * @param text
281  *            The text to replace the button with
282  */
283 function enhanceDeleteReplyButton(buttonId, replyId, text) {
284         enhanceDeleteButton(buttonId, text, function() {
285                 $.getJSON("ajax/deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
286                         if (data == null) {
287                                 return;
288                         }
289                         if (data.success) {
290                                 $("#sone .reply#" + replyId).slideUp();
291                         } else if (data.error == "invalid-reply-id") {
292                                 alert("Invalid reply ID given!");
293                         } else if (data.error == "auth-required") {
294                                 alert("You need to be logged in.");
295                         } else if (data.error == "not-authorized") {
296                                 alert("You are not allowed to delete this reply.");
297                         }
298                 }, function(xmlHttpRequest, textStatus, error) {
299                         /* ignore error. */
300                 });
301         });
302 }
303
304 function getFormPassword() {
305         return $("#sone #formPassword").text();
306 }
307
308 function getSoneElement(element) {
309         return $(element).parents(".sone");
310 }
311
312 /**
313  * Generates a list of Sones by concatening the names of the given sones with a
314  * new line character (“\n”).
315  *
316  * @param sones
317  *            The sones to format
318  * @returns {String} The created string
319  */
320 function generateSoneList(sones) {
321         var soneList = "";
322         $.each(sones, function() {
323                 if (soneList != "") {
324                         soneList += "\n";
325                 }
326                 soneList += this.name;
327         });
328         return soneList;
329 }
330
331 /**
332  * Returns the ID of the Sone that this element belongs to.
333  *
334  * @param element
335  *            The element to locate the matching Sone ID for
336  * @returns The ID of the Sone, or undefined
337  */
338 function getSoneId(element) {
339         return getSoneElement(element).find(".id").text();
340 }
341
342 function getPostElement(element) {
343         return $(element).parents(".post");
344 }
345
346 function getPostId(element) {
347         return getPostElement(element).attr("id");
348 }
349
350 function getReplyElement(element) {
351         return $(element).parents(".reply");
352 }
353
354 function getReplyId(element) {
355         return getReplyElement(element).attr("id");
356 }
357
358 function likePost(postId) {
359         $.getJSON("ajax/like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
360                 if ((data == null) || !data.success) {
361                         return;
362                 }
363                 $("#sone .post#" + postId + " > .inner-part > .status-line .like").addClass("hidden");
364                 $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").removeClass("hidden");
365                 updatePostLikes(postId);
366         }, function(xmlHttpRequest, textStatus, error) {
367                 /* ignore error. */
368         });
369 }
370
371 function unlikePost(postId) {
372         $.getJSON("ajax/unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
373                 if ((data == null) || !data.success) {
374                         return;
375                 }
376                 $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").addClass("hidden");
377                 $("#sone .post#" + postId + " > .inner-part > .status-line .like").removeClass("hidden");
378                 updatePostLikes(postId);
379         }, function(xmlHttpRequest, textStatus, error) {
380                 /* ignore error. */
381         });
382 }
383
384 function updatePostLikes(postId) {
385         $.getJSON("ajax/getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
386                 if ((data != null) && data.success) {
387                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
388                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
389                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
390                 }
391         }, function(xmlHttpRequest, textStatus, error) {
392                 /* ignore error. */
393         });
394 }
395
396 function likeReply(replyId) {
397         $.getJSON("ajax/like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
398                 if ((data == null) || !data.success) {
399                         return;
400                 }
401                 $("#sone .reply#" + replyId + " .status-line .like").addClass("hidden");
402                 $("#sone .reply#" + replyId + " .status-line .unlike").removeClass("hidden");
403                 updateReplyLikes(replyId);
404         }, function(xmlHttpRequest, textStatus, error) {
405                 /* ignore error. */
406         });
407 }
408
409 function unlikeReply(replyId) {
410         $.getJSON("ajax/unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
411                 if ((data == null) || !data.success) {
412                         return;
413                 }
414                 $("#sone .reply#" + replyId + " .status-line .unlike").addClass("hidden");
415                 $("#sone .reply#" + replyId + " .status-line .like").removeClass("hidden");
416                 updateReplyLikes(replyId);
417         }, function(xmlHttpRequest, textStatus, error) {
418                 /* ignore error. */
419         });
420 }
421
422 function updateReplyLikes(replyId) {
423         $.getJSON("ajax/getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
424                 if ((data != null) && data.success) {
425                         $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
426                         $("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
427                         $("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
428                 }
429         }, function(xmlHttpRequest, textStatus, error) {
430                 /* ignore error. */
431         });
432 }
433
434 /**
435  * Posts a reply and calls the given callback when the request finishes.
436  *
437  * @param postId
438  *            The ID of the post the reply refers to
439  * @param text
440  *            The text to post
441  * @param callbackFunction
442  *            The callback function to call when the request finishes (takes 3
443  *            parameters: success, error, replyId)
444  */
445 function postReply(postId, text, callbackFunction) {
446         $.getJSON("ajax/createReply.ajax", { "formPassword" : getFormPassword(), "post" : postId, "text": text }, function(data, textStatus) {
447                 if (data == null) {
448                         /* TODO - show error */
449                         return;
450                 }
451                 if (data.success) {
452                         callbackFunction(true, null, data.reply);
453                 } else {
454                         callbackFunction(false, data.error);
455                 }
456         }, function(xmlHttpRequest, textStatus, error) {
457                 /* ignore error. */
458         });
459 }
460
461 /**
462  * Requests information about the reply with the given ID.
463  *
464  * @param replyId
465  *            The ID of the reply
466  * @param callbackFunction
467  *            A callback function (parameters soneId, soneName, replyTime,
468  *            replyDisplayTime, text, html)
469  */
470 function getReply(replyId, callbackFunction) {
471         $.getJSON("ajax/getReply.ajax", { "reply" : replyId }, function(data, textStatus) {
472                 if ((data != null) && data.success) {
473                         callbackFunction(data.soneId, data.soneName, data.time, data.displayTime, data.text, data.html);
474                 }
475         }, function(xmlHttpRequest, textStatus, error) {
476                 /* ignore error. */
477         });
478 }
479
480 /**
481  * Ajaxifies the given notification by replacing the form with AJAX.
482  *
483  * @param notification
484  *            jQuery object representing the notification.
485  */
486 function ajaxifyNotification(notification) {
487         notification.find("form.dismiss").submit(function() {
488                 return false;
489         });
490         notification.find("form.dismiss button").click(function() {
491                 $.getJSON("ajax/dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
492                         /* dismiss in case of error, too. */
493                         notification.slideUp();
494                 }, function(xmlHttpRequest, textStatus, error) {
495                         /* ignore error. */
496                 });
497         });
498         return notification;
499 }
500
501 /**
502  * Retrieves all changed notifications.
503  */
504 function getNotifications() {
505         $.getJSON("ajax/getNotifications.ajax", {}, function(data, textStatus) {
506                 if ((data != null) && data.success) {
507                         $.each(data.notifications, function(index, value) {
508                                 oldNotification = $("#sone #notification-area .notification#" + value.id);
509                                 notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
510                                 if (oldNotification.length != 0) {
511                                         oldNotification.slideUp();
512                                         notification.insertBefore(oldNotification);
513                                 } else {
514                                         $("#sone #notification-area").append(notification);
515                                 }
516                                 notification.slideDown();
517                         });
518                         $.each(data.removedNotifications, function(index, value) {
519                                 $("#sone #notification-area .notification#" + value.id).slideUp();
520                         });
521                         setTimeout(getNotifications, 5000);
522                 } else {
523                         setTimeout(getNotifications, 30000);
524                 }
525         }, function(xmlHttpRequest, textStatus, error) {
526                 /* ignore error. */
527         });
528 }
529
530 /**
531  * Creates a new notification.
532  *
533  * @param id
534  *            The ID of the notificaiton
535  * @param text
536  *            The text of the notification
537  * @param dismissable
538  *            <code>true</code> if the notification can be dismissed by the
539  *            user
540  */
541 function createNotification(id, text, dismissable) {
542         notification = $("<div></div>").addClass("notification").attr("id", id);
543         if (dismissable) {
544                 dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
545                 dismissForm.find("input[name=notification]").val(id);
546                 notification.append(dismissForm);
547         }
548         notification.append(text);
549         return notification;
550 }