Add methods to retrieve new notification and create their HTML.
[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                 callback(data.value);
95         });
96 }
97
98 /**
99  * Fires off an AJAX request to retrieve the current status of a Sone.
100  *
101  * @param soneId
102  *            The ID of the Sone
103  * @param local
104  *            <code>true</code> if the Sone is local, <code>false</code>
105  *            otherwise
106  */
107 function getSoneStatus(soneId, local) {
108         $.getJSON("ajax/getSoneStatus.ajax", {"sone": soneId}, function(data, textStatus) {
109                 updateSoneStatus(soneId, data.name, data.status, data.modified, data.lastUpdated);
110                 /* seconds! */
111                 updateInterval = 60;
112                 if (local || data.modified || (data.status == "downloading") || (data.status == "inserting")) {
113                         updateInterval = 5;
114                 }
115                 setTimeout(function() {
116                         getSoneStatus(soneId, local);
117                 }, updateInterval * 1000);
118         });
119 }
120
121 /**
122  * Filters the given Sone ID, replacing all “~” characters by an underscore.
123  *
124  * @param soneId
125  *            The Sone ID to filter
126  * @returns The filtered Sone ID
127  */
128 function filterSoneId(soneId) {
129         return soneId.replace(/[^a-zA-Z0-9-]/g, "_");
130 }
131
132 /**
133  * Updates the status of the given Sone.
134  *
135  * @param soneId
136  *            The ID of the Sone to update
137  * @param status
138  *            The status of the Sone (“idle”, “unknown”, “inserting”,
139  *            “downloading”)
140  * @param modified
141  *            Whether the Sone is modified
142  * @param lastUpdated
143  *            The date and time of the last update (formatted for display)
144  */
145 function updateSoneStatus(soneId, name, status, modified, lastUpdated) {
146         $("#sone .sone." + filterSoneId(soneId)).
147                 toggleClass("unknown", status == "unknown").
148                 toggleClass("idle", status == "idle").
149                 toggleClass("inserting", status == "inserting").
150                 toggleClass("downloading", status == "downloading").
151                 toggleClass("modified", modified);
152         $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
153         $("#sone .sone." + filterSoneId(soneId) + " .profile-link a").text(name);
154 }
155
156 var watchedSones = {};
157
158 /**
159  * Watches this Sone for updates to its status.
160  *
161  * @param soneId
162  *            The ID of the Sone to watch
163  * @param local
164  *            <code>true</code> if the Sone is local, <code>false</code>
165  *            otherwise
166  */
167 function watchSone(soneId, local) {
168         if (watchedSones[soneId]) {
169                 return;
170         }
171         watchedSones[soneId] = true;
172         (function(soneId) {
173                 setTimeout(function() {
174                         getSoneStatus(soneId, local);
175                 }, 5000);
176         })(soneId);
177 }
178
179 /**
180  * Enhances a “delete” button so that the confirmation is done on the same page.
181  *
182  * @param buttonId
183  *            The selector of the button
184  * @param text
185  *            The text to show on the button
186  * @param deleteCallback
187  *            The callback that actually deletes something
188  */
189 function enhanceDeleteButton(buttonId, text, deleteCallback) {
190         button = $(buttonId);
191         (function(button) {
192                 newButton = $("<button></button>").addClass("confirm").hide().text(text).click(function() {
193                         $(this).fadeOut("slow");
194                         deleteCallback();
195                         return false;
196                 }).insertAfter(button);
197                 (function(button, newButton) {
198                         button.click(function() {
199                                 button.fadeOut("slow", function() {
200                                         newButton.fadeIn("slow");
201                                         $(document).one("click", function() {
202                                                 if (this != newButton.get(0)) {
203                                                         newButton.fadeOut(function() {
204                                                                 button.fadeIn();
205                                                         });
206                                                 }
207                                         });
208                                 });
209                                 return false;
210                         });
211                 })(button, newButton);
212         })(button);
213 }
214
215 /**
216  * Enhances a post’s “delete” button.
217  *
218  * @param buttonId
219  *            The selector of the button
220  * @param postId
221  *            The ID of the post to delete
222  * @param text
223  *            The text to replace the button with
224  */
225 function enhanceDeletePostButton(buttonId, postId, text) {
226         enhanceDeleteButton(buttonId, text, function() {
227                 $.getJSON("ajax/deletePost.ajax", { "post": postId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
228                         if (data.success) {
229                                 $("#sone .post#" + postId).slideUp();
230                         } else if (data.error == "invalid-post-id") {
231                                 alert("Invalid post ID given!");
232                         } else if (data.error == "auth-required") {
233                                 alert("You need to be logged in.");
234                         } else if (data.error == "not-authorized") {
235                                 alert("You are not allowed to delete this post.");
236                         }
237                 });
238         });
239 }
240
241 /**
242  * Enhances a reply’s “delete” button.
243  *
244  * @param buttonId
245  *            The selector of the button
246  * @param replyId
247  *            The ID of the reply to delete
248  * @param text
249  *            The text to replace the button with
250  */
251 function enhanceDeleteReplyButton(buttonId, replyId, text) {
252         enhanceDeleteButton(buttonId, text, function() {
253                 $.getJSON("ajax/deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
254                         if (data.success) {
255                                 $("#sone .reply#" + replyId).slideUp();
256                         } else if (data.error == "invalid-reply-id") {
257                                 alert("Invalid reply ID given!");
258                         } else if (data.error == "auth-required") {
259                                 alert("You need to be logged in.");
260                         } else if (data.error == "not-authorized") {
261                                 alert("You are not allowed to delete this reply.");
262                         }
263                 });
264         });
265 }
266
267 function getFormPassword() {
268         return $("#sone #formPassword").text();
269 }
270
271 function getSoneElement(element) {
272         return $(element).parents(".sone");
273 }
274
275 /**
276  * Generates a list of Sones by concatening the names of the given sones with a
277  * new line character (“\n”).
278  *
279  * @param sones
280  *            The sones to format
281  * @returns {String} The created string
282  */
283 function generateSoneList(sones) {
284         var soneList = "";
285         $.each(sones, function() {
286                 if (soneList != "") {
287                         soneList += "\n";
288                 }
289                 soneList += this.name;
290         });
291         return soneList;
292 }
293
294 /**
295  * Returns the ID of the Sone that this element belongs to.
296  *
297  * @param element
298  *            The element to locate the matching Sone ID for
299  * @returns The ID of the Sone, or undefined
300  */
301 function getSoneId(element) {
302         return getSoneElement(element).find(".id").text();
303 }
304
305 function getPostElement(element) {
306         return $(element).parents(".post");
307 }
308
309 function getPostId(element) {
310         return getPostElement(element).attr("id");
311 }
312
313 function getReplyElement(element) {
314         return $(element).parents(".reply");
315 }
316
317 function getReplyId(element) {
318         return getReplyElement(element).attr("id");
319 }
320
321 function likePost(postId) {
322         $.getJSON("ajax/like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function() {
323                 $("#sone .post#" + postId + " > .inner-part > .status-line .like").addClass("hidden");
324                 $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").removeClass("hidden");
325                 updatePostLikes(postId);
326         });
327 }
328
329 function unlikePost(postId) {
330         $.getJSON("ajax/unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function() {
331                 $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").addClass("hidden");
332                 $("#sone .post#" + postId + " > .inner-part > .status-line .like").removeClass("hidden");
333                 updatePostLikes(postId);
334         });
335 }
336
337 function updatePostLikes(postId) {
338         $.getJSON("ajax/getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
339                 if (data.success) {
340                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
341                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
342                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
343                 }
344         });
345 }
346
347 function likeReply(replyId) {
348         $.getJSON("ajax/like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function() {
349                 $("#sone .reply#" + replyId + " .status-line .like").addClass("hidden");
350                 $("#sone .reply#" + replyId + " .status-line .unlike").removeClass("hidden");
351                 updateReplyLikes(replyId);
352         });
353 }
354
355 function unlikeReply(replyId) {
356         $.getJSON("ajax/unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function() {
357                 $("#sone .reply#" + replyId + " .status-line .unlike").addClass("hidden");
358                 $("#sone .reply#" + replyId + " .status-line .like").removeClass("hidden");
359                 updateReplyLikes(replyId);
360         });
361 }
362
363 function updateReplyLikes(replyId) {
364         $.getJSON("ajax/getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
365                 if (data.success) {
366                         $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
367                         $("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
368                         $("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
369                 }
370         });
371 }
372
373 /**
374  * Posts a reply and calls the given callback when the request finishes.
375  *
376  * @param postId
377  *            The ID of the post the reply refers to
378  * @param text
379  *            The text to post
380  * @param callbackFunction
381  *            The callback function to call when the request finishes (takes 3
382  *            parameters: success, error, replyId)
383  */
384 function postReply(postId, text, callbackFunction) {
385         $.getJSON("ajax/createReply.ajax", { "formPassword" : getFormPassword(), "post" : postId, "text": text }, function(data, textStatus) {
386                 if (data.success) {
387                         callbackFunction(true, null, data.reply);
388                 } else {
389                         callbackFunction(false, data.error);
390                 }
391         });
392 }
393
394 /**
395  * Requests information about the reply with the given ID.
396  *
397  * @param replyId
398  *            The ID of the reply
399  * @param callbackFunction
400  *            A callback function (parameters soneId, soneName, replyTime,
401  *            replyDisplayTime, text, html)
402  */
403 function getReply(replyId, callbackFunction) {
404         $.getJSON("ajax/getReply.ajax", { "reply" : replyId }, function(data, textStatus) {
405                 if (data.success) {
406                         callbackFunction(data.soneId, data.soneName, data.time, data.displayTime, data.text, data.html);
407                 }
408         });
409 }
410
411 /**
412  * Ajaxifies the given notification by replacing the form with AJAX.
413  *
414  * @param notification
415  *            jQuery object representing the notification.
416  */
417 function ajaxifyNotification(notification) {
418         notification.find("form.dismiss").submit(function() {
419                 return false;
420         });
421         notification.find("form.dismiss button").click(function() {
422                 $.getJSON("ajax/dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
423                         notification.slideUp();
424                 });
425         });
426         return notification;
427 }
428
429 /**
430  * Retrieves all changed notifications.
431  */
432 function getNotifications() {
433         $.getJSON("ajax/getNotifications.ajax", {}, function(data, textStatus) {
434                 if (data.success) {
435                         $.each(data.notifications, function(index, value) {
436                                 oldNotification = $("#sone #notification-area .notification#" + value.id);
437                                 notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
438                                 if (oldNotification.length != 0) {
439                                         oldNotification.slideUp();
440                                         notification.insertBefore(oldNotification);
441                                 } else {
442                                         $("#sone #notification-area").append(notification);
443                                 }
444                                 notification.slideDown();
445                         });
446                 }
447                 setTimeout(getNotifications, 5000);
448         });
449 }
450
451 /**
452  * Creates a new notification.
453  *
454  * @param id
455  *            The ID of the notificaiton
456  * @param text
457  *            The text of the notification
458  * @param dismissable
459  *            <code>true</code> if the notification can be dismissed by the
460  *            user
461  */
462 function createNotification(id, text, dismissable) {
463         notification = $("<div></div>").addClass("notification").attr("id", id);
464         if (dismissable) {
465                 dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
466                 dismissForm.find("input[name=notification]").val(id);
467                 notification.append(dismissForm);
468         }
469         notification.append(text);
470         return notification;
471 }