Comments now entirely work like Facebook's comments
[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-2013 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="buttonPrimary 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 $container = $('<div class="commentOptionContainer" />').hide().insertAfter($comment.find('ul.commentResponseList'));
232 self._commentButtonList[$commentID] = $('<ul />').appendTo($container);
233
234 self._handleLoadNextResponses($commentID);
235 self._initComment($commentID, $comment);
236 self._displayedComments++;
237
238 $loadedComments = true;
239 });
240
241 if ($loadedComments) {
242 this._handleLoadNextComments();
243 }
244 },
245
246 /**
247 * Initializes a specific comment.
248 *
249 * @param integer commentID
250 * @param jQuery comment
251 */
252 _initComment: function(commentID, comment) {
253 if (this._container.data('canAdd')) {
254 this._initAddResponse(commentID, comment);
255 }
256
257 if (comment.data('canEdit')) {
258 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>');
259 $editButton.data('commentID', commentID).appendTo(comment.find('ul.commentOptions:eq(0)')).click($.proxy(this._prepareEdit, this));
260 }
261
262 if (comment.data('canDelete')) {
263 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>');
264 $deleteButton.data('commentID', commentID).appendTo(comment.find('ul.commentOptions:eq(0)')).click($.proxy(this._delete, this));
265 }
266 },
267
268 /**
269 * Initializes available responses.
270 */
271 _initResponses: function() {
272 var self = this;
273 this._container.find('.jsCommentResponse').each(function(index, response) {
274 var $response = $(response).removeClass('jsCommentResponse');
275 var $responseID = $response.data('responseID');
276 self._responses[$responseID] = $response;
277
278 self._initResponse($responseID, $response);
279 });
280 },
281
282 /**
283 * Initializes a specific response.
284 *
285 * @param integer responseID
286 * @param jQuery response
287 */
288 _initResponse: function(responseID, response) {
289 if (response.data('canEdit')) {
290 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>');
291
292 var self = this;
293 $editButton.data('responseID', responseID).appendTo(response.find('ul.commentOptions:eq(0)')).click(function(event) { self._prepareEdit(event, true); });
294 }
295
296 if (response.data('canDelete')) {
297 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>');
298
299 var self = this;
300 $deleteButton.data('responseID', responseID).appendTo(response.find('ul.commentOptions:eq(0)')).click(function(event) { self._delete(event, true); });
301 }
302 },
303
304 /**
305 * Initializes the UI components to add a comment.
306 */
307 _initAddComment: function() {
308 // create UI
309 this._commentAdd = $('<li class="box32 jsCommentAdd"><span class="framed">' + this._userAvatar + '</span><div /></li>').prependTo(this._container);
310 var $inputContainer = this._commentAdd.children('div');
311 var $input = $('<input type="text" placeholder="' + WCF.Language.get('wcf.comment.add') + '" maxlength="65535" class="long" />').appendTo($inputContainer);
312 $('<small>' + WCF.Language.get('wcf.comment.description') + '</small>').appendTo($inputContainer);
313
314 $input.keyup($.proxy(this._keyUp, this));
315 },
316
317 /**
318 * Initializes the UI elements to add a response.
319 *
320 * @param integer commentID
321 * @param jQuery comment
322 */
323 _initAddResponse: function(commentID, comment) {
324 var $placeholder = null;
325 if (!comment.data('responses') || this._loadNextResponses[commentID]) {
326 $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]);
327 }
328
329 var $listItem = $('<div class="box32 commentResponseAdd jsCommentResponseAdd"><span class="framed">' + this._userAvatar + '</span><div /></div>');
330 if ($placeholder !== null) {
331 $listItem.hide();
332 }
333 $listItem.appendTo(this._commentButtonList[commentID].parent().show());
334
335 var $inputContainer = $listItem.children('div');
336 var $input = $('<input type="text" placeholder="' + WCF.Language.get('wcf.comment.response.add') + '" maxlength="65535" class="long" />').data('commentID', commentID).appendTo($inputContainer);
337 $('<small>' + WCF.Language.get('wcf.comment.description') + '</small>').appendTo($inputContainer);
338
339 var self = this;
340 $input.keyup(function(event) { self._keyUp(event, true); });
341
342 comment.data('responsePlaceholder', $placeholder).data('responseInput', $listItem);
343 },
344
345 /**
346 * Prepares editing of a comment or response.
347 *
348 * @param object event
349 * @param boolean isResponse
350 */
351 _prepareEdit: function(event, isResponse) {
352 var $button = $(event.currentTarget);
353 var $data = {
354 objectID: this._container.data('objectID'),
355 objectTypeID: this._container.data('objectTypeID')
356 };
357
358 if (isResponse === true) {
359 $data.responseID = $button.data('responseID');
360 }
361 else {
362 $data.commentID = $button.data('commentID');
363 }
364
365 this._proxy.setOption('data', {
366 actionName: 'prepareEdit',
367 className: 'wcf\\data\\comment\\CommentAction',
368 parameters: {
369 data: $data
370 }
371 });
372 this._proxy.sendRequest();
373 },
374
375 /**
376 * Displays the UI elements to create a response.
377 *
378 * @param object event
379 */
380 _showAddResponse: function(event) {
381 var $placeholder = $(event.currentTarget);
382 var $commentID = $placeholder.data('commentID');
383 if ($placeholder.prev().hasClass('jsCommentLoadNextResponses')) {
384 this._loadResponsesExecute($commentID, true);
385 $placeholder.parent().children('.button').disable();
386 }
387
388 $placeholder.remove();
389
390 var $responseInput = this._comments[$commentID].data('responseInput').show();
391 $responseInput.find('input').focus();
392 },
393
394 /**
395 * Handles the keyup event for comments and responses.
396 *
397 * @param object event
398 * @param boolean isResponse
399 */
400 _keyUp: function(event, isResponse) {
401 // ignore every key except for [Enter] and [Esc]
402 if (event.which !== 13 && event.which !== 27) {
403 return;
404 }
405
406 var $input = $(event.currentTarget);
407
408 // cancel input
409 if (event.which === 27) {
410 $input.val('').trigger('blur', event);
411 return;
412 }
413
414 var $value = $.trim($input.val());
415
416 // ignore empty comments
417 if ($value == '') {
418 return;
419 }
420
421 var $actionName = 'addComment';
422 var $data = {
423 message: $value,
424 objectID: this._container.data('objectID'),
425 objectTypeID: this._container.data('objectTypeID')
426 };
427 if (isResponse === true) {
428 $actionName = 'addResponse';
429 $data.commentID = $input.data('commentID');
430 }
431
432 this._proxy.setOption('data', {
433 actionName: $actionName,
434 className: 'wcf\\data\\comment\\CommentAction',
435 parameters: {
436 data: $data
437 }
438 });
439 this._proxy.sendRequest();
440
441 // reset input
442 $input.val('').blur();
443 },
444
445 /**
446 * Shows a confirmation message prior to comment or response deletion.
447 *
448 * @param object event
449 * @param boolean isResponse
450 */
451 _delete: function(event, isResponse) {
452 WCF.System.Confirmation.show(WCF.Language.get('wcf.comment.delete.confirmMessage'), $.proxy(function(action) {
453 if (action === 'confirm') {
454 var $data = {
455 objectID: this._container.data('objectID'),
456 objectTypeID: this._container.data('objectTypeID')
457 };
458 if (isResponse !== true) {
459 $data.commentID = $(event.currentTarget).data('commentID');
460 }
461 else {
462 $data.responseID = $(event.currentTarget).data('responseID');
463 }
464
465 this._proxy.setOption('data', {
466 actionName: 'remove',
467 className: 'wcf\\data\\comment\\CommentAction',
468 parameters: {
469 data: $data
470 }
471 });
472 this._proxy.sendRequest();
473 }
474 }, this));
475 },
476
477 /**
478 * Handles successful AJAX requests.
479 *
480 * @param object data
481 * @param string textStatus
482 * @param jQuery jqXHR
483 */
484 _success: function(data, textStatus, jqXHR) {
485 switch (data.actionName) {
486 case 'addComment':
487 $(data.returnValues.template).insertAfter(this._commentAdd).wcfFadeIn();
488 break;
489
490 case 'addResponse':
491 $(data.returnValues.template).appendTo(this._comments[data.returnValues.commentID].find('ul.commentResponseList')).wcfFadeIn();
492 break;
493
494 case 'edit':
495 this._update(data);
496 break;
497
498 case 'loadComments':
499 this._insertComments(data);
500 break;
501
502 case 'loadResponses':
503 this._insertResponses(data);
504 break;
505
506 case 'prepareEdit':
507 this._edit(data);
508 break;
509
510 case 'remove':
511 this._remove(data);
512 break;
513 }
514
515 WCF.DOMNodeInsertedHandler.execute();
516 },
517
518 /**
519 * Inserts previously loaded comments.
520 *
521 * @param object data
522 */
523 _insertComments: function(data) {
524 // insert comments
525 $(data.returnValues.template).insertBefore(this._loadNextComments);
526
527 // update time of last comment
528 this._container.data('lastCommentTime', data.returnValues.lastCommentTime);
529 },
530
531 /**
532 * Inserts previously loaded responses.
533 *
534 * @param object data
535 */
536 _insertResponses: function(data) {
537 var $comment = this._comments[data.returnValues.commentID];
538
539 // insert responses
540 $(data.returnValues.template).appendTo($comment.find('ul.commentResponseList'));
541
542 // update time of last response
543 $comment.data('lastResponseTime', data.returnValues.lastResponseTime);
544
545 // update button state to load next responses
546 this._handleLoadNextResponses(data.returnValues.commentID);
547 },
548
549 /**
550 * Removes a comment or response from list.
551 *
552 * @param object data
553 */
554 _remove: function(data) {
555 if (data.returnValues.commentID) {
556 this._comments[data.returnValues.commentID].remove();
557 delete this._comments[data.returnValues.commentID];
558 }
559 else {
560 this._responses[data.returnValues.responseID].remove();
561 delete this._responses[data.returnValues.responseID];
562 }
563 },
564
565 /**
566 * Prepares editing of a comment or response.
567 *
568 * @param object data
569 */
570 _edit: function(data) {
571 if (data.returnValues.commentID) {
572 var $content = this._comments[data.returnValues.commentID].find('.commentContent:eq(0) .userMessage:eq(0)');
573 }
574 else {
575 var $content = this._responses[data.returnValues.responseID].find('.commentContent:eq(0) .userMessage:eq(0)');
576 }
577
578 // replace content with input field
579 $content.html($.proxy(function(index, oldHTML) {
580 var $input = $('<input type="text" class="long" maxlength="65535" /><small>' + WCF.Language.get('wcf.comment.description') + '</small>').val(data.returnValues.message);
581 $input.data('__html', oldHTML).keyup($.proxy(this._saveEdit, this));
582
583 if (data.returnValues.commentID) {
584 $input.data('commentID', data.returnValues.commentID);
585 }
586 else {
587 $input.data('responseID', data.returnValues.responseID);
588 }
589
590 return $input;
591 }, this));
592 $content.children('input').focus();
593
594 // hide elements
595 $content.parent().find('.containerHeadline:eq(0)').hide();
596 $content.parent().find('.buttonGroupNavigation:eq(0)').hide();
597 },
598
599 /**
600 * Updates a comment or response.
601 *
602 * @param object data
603 */
604 _update: function(data) {
605 if (data.returnValues.commentID) {
606 var $input = this._comments[data.returnValues.commentID].find('.commentContent:eq(0) .userMessage:eq(0) > input');
607 }
608 else {
609 var $input = this._responses[data.returnValues.responseID].find('.commentContent:eq(0) .userMessage:eq(0) > input');
610 }
611
612 $input.data('__html', data.returnValues.message);
613
614 this._cancelEdit($input);
615 },
616
617 /**
618 * Saves editing of a comment or response.
619 *
620 * @param object event
621 */
622 _saveEdit: function(event) {
623 var $input = $(event.currentTarget);
624
625 // abort with [Esc]
626 if (event.which === 27) {
627 this._cancelEdit($input);
628 return;
629 }
630 else if (event.which !== 13) {
631 // ignore everything except for [Enter]
632 return;
633 }
634
635 var $message = $.trim($input.val());
636
637 // ignore empty message
638 if ($message === '') {
639 return;
640 }
641
642 var $data = {
643 message: $message,
644 objectID: this._container.data('objectID'),
645 objectTypeID: this._container.data('objectTypeID')
646 };
647 if ($input.data('commentID')) {
648 $data.commentID = $input.data('commentID');
649 }
650 else {
651 $data.responseID = $input.data('responseID');
652 }
653
654 this._proxy.setOption('data', {
655 actionName: 'edit',
656 className: 'wcf\\data\\comment\\CommentAction',
657 parameters: {
658 data: $data
659 }
660 });
661 this._proxy.sendRequest()
662 },
663
664 /**
665 * Cancels editing of a comment or response.
666 *
667 * @param jQuery input
668 */
669 _cancelEdit: function(input) {
670 // restore elements
671 input.parent().prev('.containerHeadline:eq(0)').show();
672 input.parent().next('.buttonGroupNavigation:eq(0)').show();
673
674 // restore HTML
675 input.parent().html(input.data('__html'));
676 }
677 });
678
679 /**
680 * Like support for comments
681 *
682 * @see WCF.Like
683 */
684 WCF.Comment.Like = WCF.Like.extend({
685 /**
686 * @see WCF.Like._getContainers()
687 */
688 _getContainers: function() {
689 return $('.commentList > li.comment');
690 },
691
692 /**
693 * @see WCF.Like._getObjectID()
694 */
695 _getObjectID: function(containerID) {
696 return this._containers[containerID].data('commentID');
697 },
698
699 /**
700 * @see WCF.Like._buildWidget()
701 */
702 _buildWidget: function(containerID, likeButton, dislikeButton, badge, summary) {
703 this._containers[containerID].find('.containerHeadline:eq(0) > h3').append(badge);
704
705 if (this._canLike) {
706 likeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
707 dislikeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
708 }
709 },
710
711 /**
712 * @see WCF.Like._getWidgetContainer()
713 */
714 _getWidgetContainer: function(containerID) {},
715
716 /**
717 * @see WCF.Like._addWidget()
718 */
719 _addWidget: function(containerID, widget) {}
720 });
721
722 /**
723 * Namespace for comment responses
724 */
725 WCF.Comment.Response = { };
726
727 /**
728 * Like support for comments responses.
729 *
730 * @see WCF.Like
731 */
732 WCF.Comment.Response.Like = WCF.Like.extend({
733 /**
734 * @see WCF.Like._addWidget()
735 */
736 _addWidget: function(containerID, widget) { },
737
738 /**
739 * @see WCF.Like._buildWidget()
740 */
741 _buildWidget: function(containerID, likeButton, dislikeButton, badge, summary) {
742 this._containers[containerID].find('.containerHeadline:eq(0) > h3').append(badge);
743
744 if (this._canLike) {
745 likeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
746 dislikeButton.appendTo(this._containers[containerID].find('.commentOptions:eq(0)'));
747 }
748 },
749
750 /**
751 * @see WCF.Like._getContainers()
752 */
753 _getContainers: function() {
754 return $('.commentResponseList > li.commentResponse');
755 },
756
757 /**
758 * @see WCF.Like._getObjectID()
759 */
760 _getObjectID: function(containerID) {
761 return this._containers[containerID].data('responseID');
762 },
763
764 /**
765 * @see WCF.Like._getWidgetContainer()
766 */
767 _getWidgetContainer: function(containerID) { }
768 });