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