Fixed time zone calculation issue
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WCF.Comment.js
1 /**
2 * Namespace for comments
3 */
4 WCF.Comment = {};
5
6 /**
7 * Comment support for WCF
8 *
9 * @author Alexander Ebert
10 * @copyright 2001-2014 WoltLab GmbH
11 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
12 */
13 WCF.Comment.Handler = Class.extend({
14 /**
15 * input element to add a comment
16 * @var jQuery
17 */
18 _commentAdd: null,
19
20 /**
21 * list of comment buttons per comment
22 * @var object
23 */
24 _commentButtonList: { },
25
26 /**
27 * list of comment objects
28 * @var object
29 */
30 _comments: { },
31
32 /**
33 * comment container object
34 * @var jQuery
35 */
36 _container: null,
37
38 /**
39 * container id
40 * @var string
41 */
42 _containerID: '',
43
44 /**
45 * number of currently displayed comments
46 * @var integer
47 */
48 _displayedComments: 0,
49
50 /**
51 * button to load next comments
52 * @var jQuery
53 */
54 _loadNextComments: null,
55
56 /**
57 * buttons to load next responses per comment
58 * @var object
59 */
60 _loadNextResponses: { },
61
62 /**
63 * action proxy
64 * @var WCF.Action.Proxy
65 */
66 _proxy: null,
67
68 /**
69 * list of response objects
70 * @var object
71 */
72 _responses: { },
73
74 /**
75 * user's avatar
76 * @var string
77 */
78 _userAvatar: '',
79
80 /**
81 * Initializes the WCF.Comment.Handler class.
82 *
83 * @param string containerID
84 * @param string userAvatar
85 */
86 init: function(containerID, userAvatar) {
87 this._commentAdd = null;
88 this._commentButtonList = { };
89 this._comments = { };
90 this._containerID = containerID;
91 this._displayedComments = 0;
92 this._loadNextComments = null;
93 this._loadNextResponses = { };
94 this._responses = { };
95 this._userAvatar = userAvatar;
96
97 this._container = $('#' + $.wcfEscapeID(this._containerID));
98 if (!this._container.length) {
99 console.debug("[WCF.Comment.Handler] Unable to find container identified by '" + this._containerID + "'");
100 }
101
102 this._proxy = new WCF.Action.Proxy({
103 success: $.proxy(this._success, this)
104 });
105
106 this._initComments();
107 this._initResponses();
108
109 // add new comment
110 if (this._container.data('canAdd')) {
111 this._initAddComment();
112 }
113
114 WCF.DOMNodeInsertedHandler.execute();
115 WCF.DOMNodeInsertedHandler.addCallback('WCF.Comment.Handler', $.proxy(this._domNodeInserted, this));
116 },
117
118 /**
119 * Shows a button to load next comments.
120 */
121 _handleLoadNextComments: function() {
122 if (this._displayedComments < this._container.data('comments')) {
123 if (this._loadNextComments === null) {
124 this._loadNextComments = $('<li class="commentLoadNext"><button class="small">' + WCF.Language.get('wcf.comment.more') + '</button></li>').appendTo(this._container);
125 this._loadNextComments.children('button').click($.proxy(this._loadComments, this));
126 }
127
128 this._loadNextComments.children('button').enable();
129 }
130 else if (this._loadNextComments !== null) {
131 this._loadNextComments.hide();
132 }
133 },
134
135 /**
136 * Shows a button to load next responses per comment.
137 *
138 * @param integer commentID
139 */
140 _handleLoadNextResponses: function(commentID) {
141 var $comment = this._comments[commentID];
142 $comment.data('displayedResponses', $comment.find('ul.commentResponseList > li').length);
143
144 if ($comment.data('displayedResponses') < $comment.data('responses')) {
145 if (this._loadNextResponses[commentID] === undefined) {
146 var $difference = $comment.data('responses') - $comment.data('displayedResponses');
147 this._loadNextResponses[commentID] = $('<li class="jsCommentLoadNextResponses"><a>' + WCF.Language.get('wcf.comment.response.more', { count: $difference }) + '</a></li>').appendTo(this._commentButtonList[commentID]);
148 this._loadNextResponses[commentID].children('a').data('commentID', commentID).click($.proxy(this._loadResponses, this));
149 this._commentButtonList[commentID].parent().show();
150 }
151 }
152 else if (this._loadNextResponses[commentID] !== undefined) {
153 var $showAddResponse = this._loadNextResponses[commentID].next();
154 this._loadNextResponses[commentID].remove();
155 if ($showAddResponse.length) {
156 $showAddResponse.trigger('click');
157 }
158 }
159 },
160
161 /**
162 * Loads next comments.
163 */
164 _loadComments: function() {
165 this._loadNextComments.children('button').disable();
166
167 this._proxy.setOption('data', {
168 actionName: 'loadComments',
169 className: 'wcf\\data\\comment\\CommentAction',
170 parameters: {
171 data: {
172 objectID: this._container.data('objectID'),
173 objectTypeID: this._container.data('objectTypeID'),
174 lastCommentTime: this._container.data('lastCommentTime')
175 }
176 }
177 });
178 this._proxy.sendRequest();
179 },
180
181 /**
182 * Loads next responses for given comment.
183 *
184 * @param object event
185 */
186 _loadResponses: function(event) {
187 this._loadResponsesExecute($(event.currentTarget).disable().data('commentID'), false);
188
189 },
190
191 /**
192 * Executes loading of comments, optionally fetching all at once.
193 *
194 * @param integer commentID
195 * @param boolean loadAllResponses
196 */
197 _loadResponsesExecute: function(commentID, loadAllResponses) {
198 this._proxy.setOption('data', {
199 actionName: 'loadResponses',
200 className: 'wcf\\data\\comment\\response\\CommentResponseAction',
201 parameters: {
202 data: {
203 commentID: commentID,
204 lastResponseTime: this._comments[commentID].data('lastResponseTime'),
205 loadAllResponses: (loadAllResponses ? 1 : 0)
206 }
207 }
208 });
209 this._proxy.sendRequest();
210 },
211
212 /**
213 * Handles DOMNodeInserted events.
214 */
215 _domNodeInserted: function() {
216 this._initComments();
217 this._initResponses();
218 },
219
220 /**
221 * Initializes available comments.
222 */
223 _initComments: function() {
224 var self = this;
225 var $loadedComments = false;
226 this._container.find('.jsComment').each(function(index, comment) {
227 var $comment = $(comment).removeClass('jsComment');
228 var $commentID = $comment.data('commentID');
229 self._comments[$commentID] = $comment;
230
231 var $insertAfter = $comment.find('ul.commentResponseList');
232 if (!$insertAfter.length) $insertAfter = $comment.find('.commentContent');
233
234 $container = $('<div class="commentOptionContainer" />').hide().insertAfter($insertAfter);
235 self._commentButtonList[$commentID] = $('<ul />').appendTo($container);
236
237 self._handleLoadNextResponses($commentID);
238 self._initComment($commentID, $comment);
239 self._displayedComments++;
240
241 $loadedComments = true;
242 });
243
244 if ($loadedComments) {
245 this._handleLoadNextComments();
246 }
247 },
248
249 /**
250 * Initializes a specific comment.
251 *
252 * @param integer commentID
253 * @param jQuery comment
254 */
255 _initComment: function(commentID, comment) {
256 if (this._container.data('canAdd')) {
257 this._initAddResponse(commentID, comment);
258 }
259
260 if (comment.data('canEdit')) {
261 var $editButton = $('<li><a class="jsTooltip" title="' + WCF.Language.get('wcf.global.button.edit') + '"><span class="icon icon16 icon-pencil" /> <span class="invisible">' + WCF.Language.get('wcf.global.button.edit') + '</span></a></li>');
262 $editButton.data('commentID', commentID).appendTo(comment.find('ul.commentOptions:eq(0)')).click($.proxy(this._prepareEdit, this));
263 }
264
265 if (comment.data('canDelete')) {
266 var $deleteButton = $('<li><a class="jsTooltip" title="' + WCF.Language.get('wcf.global.button.delete') + '"><span class="icon icon16 icon-remove" /> <span class="invisible">' + WCF.Language.get('wcf.global.button.delete') + '</span></a></li>');
267 $deleteButton.data('commentID', commentID).appendTo(comment.find('ul.commentOptions:eq(0)')).click($.proxy(this._delete, this));
268 }
269 },
270
271 /**
272 * Initializes available responses.
273 */
274 _initResponses: function() {
275 var self = this;
276 this._container.find('.jsCommentResponse').each(function(index, response) {
277 var $response = $(response).removeClass('jsCommentResponse');
278 var $responseID = $response.data('responseID');
279 self._responses[$responseID] = $response;
280
281 self._initResponse($responseID, $response);
282 });
283 },
284
285 /**
286 * Initializes a specific response.
287 *
288 * @param integer responseID
289 * @param jQuery response
290 */
291 _initResponse: function(responseID, response) {
292 if (response.data('canEdit')) {
293 var $editButton = $('<li><a class="jsTooltip" title="' + WCF.Language.get('wcf.global.button.edit') + '"><span class="icon icon16 icon-pencil" /> <span class="invisible">' + WCF.Language.get('wcf.global.button.edit') + '</span></a></li>');
294
295 var self = this;
296 $editButton.data('responseID', responseID).appendTo(response.find('ul.commentOptions:eq(0)')).click(function(event) { self._prepareEdit(event, true); });
297 }
298
299 if (response.data('canDelete')) {
300 var $deleteButton = $('<li><a class="jsTooltip" title="' + WCF.Language.get('wcf.global.button.delete') + '"><span class="icon icon16 icon-remove" /> <span class="invisible">' + WCF.Language.get('wcf.global.button.delete') + '</span></a></li>');
301
302 var self = this;
303 $deleteButton.data('responseID', responseID).appendTo(response.find('ul.commentOptions:eq(0)')).click(function(event) { self._delete(event, true); });
304 }
305 },
306
307 /**
308 * Initializes the UI components to add a comment.
309 */
310 _initAddComment: function() {
311 // create UI
312 this._commentAdd = $('<li class="box32 jsCommentAdd"><span class="framed">' + this._userAvatar + '</span><div /></li>').prependTo(this._container);
313 var $inputContainer = this._commentAdd.children('div');
314 var $input = $('<input type="text" placeholder="' + WCF.Language.get('wcf.comment.add') + '" maxlength="65535" class="long" />').appendTo($inputContainer);
315 $('<small>' + WCF.Language.get('wcf.comment.description') + '</small>').appendTo($inputContainer);
316
317 $input.keyup($.proxy(this._keyUp, this));
318 },
319
320 /**
321 * Initializes the UI elements to add a response.
322 *
323 * @param integer commentID
324 * @param jQuery comment
325 */
326 _initAddResponse: function(commentID, comment) {
327 var $placeholder = null;
328 if (!comment.data('responses') || this._loadNextResponses[commentID]) {
329 $placeholder = $('<li class="jsCommentShowAddResponse"><a>' + WCF.Language.get('wcf.comment.button.response.add') + '</a></li>').data('commentID', commentID).click($.proxy(this._showAddResponse, this)).appendTo(this._commentButtonList[commentID]);
330 }
331
332 var $listItem = $('<div class="box32 commentResponseAdd jsCommentResponseAdd"><span class="framed">' + this._userAvatar + '</span><div /></div>');
333 if ($placeholder !== null) {
334 $listItem.hide();
335 }
336 else {
337 this._commentButtonList[commentID].parent().addClass('jsAddResponseActive');
338 }
339 $listItem.appendTo(this._commentButtonList[commentID].parent().show());
340
341 var $inputContainer = $listItem.children('div');
342 var $input = $('<input type="text" placeholder="' + WCF.Language.get('wcf.comment.response.add') + '" maxlength="65535" class="long" />').data('commentID', commentID).appendTo($inputContainer);
343 $('<small>' + WCF.Language.get('wcf.comment.description') + '</small>').appendTo($inputContainer);
344
345 var self = this;
346 $input.keyup(function(event) { self._keyUp(event, true); });
347
348 comment.data('responsePlaceholder', $placeholder).data('responseInput', $listItem);
349 },
350
351 /**
352 * Prepares editing of a comment or response.
353 *
354 * @param object event
355 * @param boolean isResponse
356 */
357 _prepareEdit: function(event, isResponse) {
358 var $button = $(event.currentTarget);
359 var $data = {
360 objectID: this._container.data('objectID'),
361 objectTypeID: this._container.data('objectTypeID')
362 };
363
364 if (isResponse === true) {
365 $data.responseID = $button.data('responseID');
366 }
367 else {
368 $data.commentID = $button.data('commentID');
369 }
370
371 this._proxy.setOption('data', {
372 actionName: 'prepareEdit',
373 className: 'wcf\\data\\comment\\CommentAction',
374 parameters: {
375 data: $data
376 }
377 });
378 this._proxy.sendRequest();
379 },
380
381 /**
382 * Displays the UI elements to create a response.
383 *
384 * @param object event
385 */
386 _showAddResponse: function(event) {
387 var $placeholder = $(event.currentTarget);
388 var $commentID = $placeholder.data('commentID');
389 if ($placeholder.prev().hasClass('jsCommentLoadNextResponses')) {
390 this._loadResponsesExecute($commentID, true);
391 $placeholder.parent().children('.button').disable();
392 }
393
394 $placeholder.remove();
395
396 var $responseInput = this._comments[$commentID].data('responseInput').show();
397 $responseInput.find('input').focus();
398
399 $responseInput.parents('.commentOptionContainer').addClass('jsAddResponseActive');
400 },
401
402 /**
403 * Handles the keyup event for comments and responses.
404 *
405 * @param object event
406 * @param boolean isResponse
407 */
408 _keyUp: function(event, isResponse) {
409 // ignore every key except for [Enter] and [Esc]
410 if (event.which !== 13 && event.which !== 27) {
411 return;
412 }
413
414 var $input = $(event.currentTarget);
415
416 // cancel input
417 if (event.which === 27) {
418 $input.val('').trigger('blur', event);
419 return;
420 }
421
422 var $value = $.trim($input.val());
423
424 // ignore empty comments
425 if ($value == '') {
426 return;
427 }
428
429 var $actionName = 'addComment';
430 var $data = {
431 message: $value,
432 objectID: this._container.data('objectID'),
433 objectTypeID: this._container.data('objectTypeID')
434 };
435 if (isResponse === true) {
436 $actionName = 'addResponse';
437 $data.commentID = $input.data('commentID');
438 }
439
440 this._proxy.setOption('data', {
441 actionName: $actionName,
442 className: 'wcf\\data\\comment\\CommentAction',
443 parameters: {
444 data: $data
445 }
446 });
447 this._proxy.sendRequest();
448
449 // reset input
450 //$input.val('').blur();
451 },
452
453 /**
454 * Shows a confirmation message prior to comment or response deletion.
455 *
456 * @param object event
457 * @param boolean isResponse
458 */
459 _delete: function(event, isResponse) {
460 WCF.System.Confirmation.show(WCF.Language.get('wcf.comment.delete.confirmMessage'), $.proxy(function(action) {
461 if (action === 'confirm') {
462 var $data = {
463 objectID: this._container.data('objectID'),
464 objectTypeID: this._container.data('objectTypeID')
465 };
466 if (isResponse !== true) {
467 $data.commentID = $(event.currentTarget).data('commentID');
468 }
469 else {
470 $data.responseID = $(event.currentTarget).data('responseID');
471 }
472
473 this._proxy.setOption('data', {
474 actionName: 'remove',
475 className: 'wcf\\data\\comment\\CommentAction',
476 parameters: {
477 data: $data
478 }
479 });
480 this._proxy.sendRequest();
481 }
482 }, this));
483 },
484
485 /**
486 * Handles successful AJAX requests.
487 *
488 * @param object data
489 * @param string textStatus
490 * @param jQuery jqXHR
491 */
492 _success: function(data, textStatus, jqXHR) {
493 switch (data.actionName) {
494 case 'addComment':
495 this._commentAdd.find('input').val('').blur();
496 $(data.returnValues.template).insertAfter(this._commentAdd).wcfFadeIn();
497 break;
498
499 case 'addResponse':
500 var $comment = this._comments[data.returnValues.commentID];
501 $comment.find('.jsCommentResponseAdd input').val('').blur();
502
503 var $responseList = $comment.find('ul.commentResponseList');
504 if (!$responseList.length) $responseList = $('<ul class="commentResponseList" />').insertBefore($comment.find('.commentOptionContainer'));
505 $(data.returnValues.template).appendTo($responseList).wcfFadeIn();
506 break;
507
508 case 'edit':
509 this._update(data);
510 break;
511
512 case 'loadComments':
513 this._insertComments(data);
514 break;
515
516 case 'loadResponses':
517 this._insertResponses(data);
518 break;
519
520 case 'prepareEdit':
521 this._edit(data);
522 break;
523
524 case 'remove':
525 this._remove(data);
526 break;
527 }
528
529 WCF.DOMNodeInsertedHandler.execute();
530 },
531
532 /**
533 * Inserts previously loaded comments.
534 *
535 * @param object data
536 */
537 _insertComments: function(data) {
538 // insert comments
539 $(data.returnValues.template).insertBefore(this._loadNextComments);
540
541 // update time of last comment
542 this._container.data('lastCommentTime', data.returnValues.lastCommentTime);
543 },
544
545 /**
546 * Inserts previously loaded responses.
547 *
548 * @param object data
549 */
550 _insertResponses: function(data) {
551 var $comment = this._comments[data.returnValues.commentID];
552
553 // insert responses
554 $(data.returnValues.template).appendTo($comment.find('ul.commentResponseList'));
555
556 // update time of last response
557 $comment.data('lastResponseTime', data.returnValues.lastResponseTime);
558
559 // update button state to load next responses
560 this._handleLoadNextResponses(data.returnValues.commentID);
561 },
562
563 /**
564 * Removes a comment or response from list.
565 *
566 * @param object data
567 */
568 _remove: function(data) {
569 if (data.returnValues.commentID) {
570 this._comments[data.returnValues.commentID].remove();
571 delete this._comments[data.returnValues.commentID];
572 }
573 else {
574 this._responses[data.returnValues.responseID].remove();
575 delete this._responses[data.returnValues.responseID];
576 }
577 },
578
579 /**
580 * Prepares editing of a comment or response.
581 *
582 * @param object data
583 */
584 _edit: function(data) {
585 if (data.returnValues.commentID) {
586 var $content = this._comments[data.returnValues.commentID].find('.commentContent:eq(0) .userMessage:eq(0)');
587 }
588 else {
589 var $content = this._responses[data.returnValues.responseID].find('.commentContent:eq(0) .userMessage:eq(0)');
590 }
591
592 // replace content with input field
593 $content.html($.proxy(function(index, oldHTML) {
594 var $input = $('<input type="text" class="long" maxlength="65535" /><small>' + WCF.Language.get('wcf.comment.description') + '</small>').val(data.returnValues.message);
595 $input.data('__html', oldHTML).keyup($.proxy(this._saveEdit, this));
596
597 if (data.returnValues.commentID) {
598 $input.data('commentID', data.returnValues.commentID);
599 }
600 else {
601 $input.data('responseID', data.returnValues.responseID);
602 }
603
604 return $input;
605 }, this));
606 $content.children('input').focus();
607
608 // hide elements
609 $content.parent().find('.containerHeadline:eq(0)').hide();
610 $content.parent().find('.buttonGroupNavigation:eq(0)').hide();
611 },
612
613 /**
614 * Updates a comment or response.
615 *
616 * @param object data
617 */
618 _update: function(data) {
619 if (data.returnValues.commentID) {
620 var $input = this._comments[data.returnValues.commentID].find('.commentContent:eq(0) .userMessage:eq(0) > input');
621 }
622 else {
623 var $input = this._responses[data.returnValues.responseID].find('.commentContent:eq(0) .userMessage:eq(0) > input');
624 }
625
626 $input.data('__html', data.returnValues.message);
627
628 this._cancelEdit($input);
629 },
630
631 /**
632 * Saves editing of a comment or response.
633 *
634 * @param object event
635 */
636 _saveEdit: function(event) {
637 var $input = $(event.currentTarget);
638
639 // abort with [Esc]
640 if (event.which === 27) {
641 this._cancelEdit($input);
642 return;
643 }
644 else if (event.which !== 13) {
645 // ignore everything except for [Enter]
646 return;
647 }
648
649 var $message = $.trim($input.val());
650
651 // ignore empty message
652 if ($message === '') {
653 return;
654 }
655
656 var $data = {
657 message: $message,
658 objectID: this._container.data('objectID'),
659 objectTypeID: this._container.data('objectTypeID')
660 };
661 if ($input.data('commentID')) {
662 $data.commentID = $input.data('commentID');
663 }
664 else {
665 $data.responseID = $input.data('responseID');
666 }
667
668 this._proxy.setOption('data', {
669 actionName: 'edit',
670 className: 'wcf\\data\\comment\\CommentAction',
671 parameters: {
672 data: $data
673 }
674 });
675 this._proxy.sendRequest()
676 },
677
678 /**
679 * Cancels editing of a comment or response.
680 *
681 * @param jQuery input
682 */
683 _cancelEdit: function(input) {
684 // restore elements
685 input.parent().prev('.containerHeadline:eq(0)').show();
686 input.parent().next('.buttonGroupNavigation:eq(0)').show();
687
688 // restore HTML
689 input.parent().html(input.data('__html'));
690 }
691 });
692
693 /**
694 * Like support for comments
695 *
696 * @see WCF.Like
697 */
698 WCF.Comment.Like = WCF.Like.extend({
699 /**
700 * @see WCF.Like._getContainers()
701 */
702 _getContainers: function() {
703 return $('.commentList > li.comment');
704 },
705
706 /**
707 * @see WCF.Like._getObjectID()
708 */
709 _getObjectID: function(containerID) {
710 return this._containers[containerID].data('commentID');
711 },
712
713 /**
714 * @see WCF.Like._buildWidget()
715 */
716 _buildWidget: function(containerID, likeButton, dislikeButton, badge, summary) {
717 this._containers[containerID].find('.containerHeadline:eq(0) > h3').append(badge);
718
719 if (this._canLike) {
720 likeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
721 dislikeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
722 }
723 },
724
725 /**
726 * @see WCF.Like._getWidgetContainer()
727 */
728 _getWidgetContainer: function(containerID) {},
729
730 /**
731 * @see WCF.Like._addWidget()
732 */
733 _addWidget: function(containerID, widget) {}
734 });
735
736 /**
737 * Namespace for comment responses
738 */
739 WCF.Comment.Response = { };
740
741 /**
742 * Like support for comments responses.
743 *
744 * @see WCF.Like
745 */
746 WCF.Comment.Response.Like = WCF.Like.extend({
747 /**
748 * @see WCF.Like._addWidget()
749 */
750 _addWidget: function(containerID, widget) { },
751
752 /**
753 * @see WCF.Like._buildWidget()
754 */
755 _buildWidget: function(containerID, likeButton, dislikeButton, badge, summary) {
756 this._containers[containerID].find('.containerHeadline:eq(0) > h3').append(badge);
757
758 if (this._canLike) {
759 likeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
760 dislikeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
761 }
762 },
763
764 /**
765 * @see WCF.Like._getContainers()
766 */
767 _getContainers: function() {
768 return $('.commentResponseList > li.commentResponse');
769 },
770
771 /**
772 * @see WCF.Like._getObjectID()
773 */
774 _getObjectID: function(containerID) {
775 return this._containers[containerID].data('responseID');
776 },
777
778 /**
779 * @see WCF.Like._getWidgetContainer()
780 */
781 _getWidgetContainer: function(containerID) { }
782 });