/**
* 2017 Zemez
*
* JX Blog Comment
*
* NOTICE OF LICENSE
*
* This source file is subject to the General Public License (GPL 2.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/GPL-2.0
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade the module to newer
* versions in the future.
*
* @author Zemez (Alexander Grosul)
* @copyright 2017 Zemez
* @license http://opensource.org/licenses/GPL-2.0 General Public License (GPL 2.0)
*/
// jquery-comments.js 1.2.0
// (c) 2017 Joona Tykkyläinen, Viima Solutions Oy
// jquery-comments may be freely distributed under the MIT license.
// For all details and documentation:
// http://viima.github.io/jquery-comments/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node/CommonJS
module.exports = function(root, jQuery) {
if (jQuery === undefined) {
// require('jQuery') returns a factory that requires window to
// build a jQuery instance, we normalize how we use modules
// that require this pattern but the window provided is a noop
// if it's defined (how jquery works)
if (typeof window !== 'undefined') {
jQuery = require('jquery');
}
else {
jQuery = require('jquery')(root);
}
}
factory(jQuery);
return jQuery;
};
} else {
// Browser globals
factory(jQuery);
}
}(function($) {
var Comments = {
// Instance variables
// ==================
$el: null,
commentsById: {},
usersById: {},
dataFetched: false,
currentSortKey: '',
options: {},
events: {
// Close dropdowns
'click': 'closeDropdowns',
// Save comment on keydown
'keydown [contenteditable]' : 'saveOnKeydown',
// Listening changes in contenteditable fields (due to input event not working with IE)
'focus [contenteditable]' : 'saveEditableContent',
'keyup [contenteditable]' : 'checkEditableContentForChange',
'paste [contenteditable]' : 'checkEditableContentForChange',
'input [contenteditable]' : 'checkEditableContentForChange',
'blur [contenteditable]' : 'checkEditableContentForChange',
// Navigation
'click .navigation li[data-sort-key]' : 'navigationElementClicked',
'click .navigation li.title' : 'toggleNavigationDropdown',
// Main comenting field
'click .commenting-field.main .textarea': 'showMainCommentingField',
'click .commenting-field.main .close' : 'hideMainCommentingField',
// All commenting fields
'click .commenting-field .textarea' : 'increaseTextareaHeight',
'change .commenting-field .textarea' : 'increaseTextareaHeight textareaContentChanged',
'click .commenting-field:not(.main) .close' : 'removeCommentingField',
// Edit mode actions
'click .commenting-field .send.enabled' : 'postComment',
'click .commenting-field .update.enabled' : 'putComment',
'click .commenting-field .delete.enabled' : 'deleteComment',
'change .commenting-field .upload.enabled input[type="file"]' : 'fileInputChanged',
// Other actions
'click li.comment button.upvote' : 'upvoteComment',
'click li.comment button.delete.enabled' : 'deleteComment',
'click li.comment .hashtag' : 'hashtagClicked',
'click li.comment .ping' : 'pingClicked',
// Other
'click li.comment ul.child-comments .toggle-all': 'toggleReplies',
'click li.comment button.reply': 'replyButtonClicked',
'click li.comment button.edit': 'editButtonClicked',
// Drag & dropping attachments
'dragenter' : 'showDroppableOverlay',
'dragenter .droppable-overlay' : 'handleDragEnter',
'dragleave .droppable-overlay' : 'handleDragLeaveForOverlay',
'dragenter .droppable-overlay .droppable' : 'handleDragEnter',
'dragleave .droppable-overlay .droppable' : 'handleDragLeaveForDroppable',
'dragover .droppable-overlay' : 'handleDragOverForOverlay',
'drop .droppable-overlay' : 'handleDrop',
// Prevent propagating the click event into buttons under the autocomplete dropdown
'click .dropdown.autocomplete': 'stopPropagation',
'mousedown .dropdown.autocomplete': 'stopPropagation',
'touchstart .dropdown.autocomplete': 'stopPropagation',
},
// Default options
// ===============
getDefaultOptions: function() {
return {
// User
profilePictureURL: '',
currentUserIsAdmin: false,
currentUserId: null,
// Font awesome icon overrides
spinnerIconURL: '',
upvoteIconURL: '',
replyIconURL: '',
uploadIconURL: '',
attachmentIconURL: '',
fileIconURL: '',
noCommentsIconURL: '',
// Strings to be formatted (for example localization)
textareaPlaceholderText: 'Add a comment',
newestText: 'Newest',
oldestText: 'Oldest',
popularText: 'Popular',
attachmentsText: 'Attachments',
sendText: 'Send',
replyText: 'Reply',
editText: 'Edit',
editedText: 'Edited',
youText: 'You',
saveText: 'Save',
deleteText: 'Delete',
newText: 'New',
viewAllRepliesText: 'View all __replyCount__ replies',
hideRepliesText: 'Hide replies',
noCommentsText: 'No comments',
noAttachmentsText: 'No attachments',
attachmentDropText: 'Drop files here',
textFormatter: function(text) {return text},
// Functionalities
enableReplying: true,
enableEditing: true,
enableUpvoting: true,
enableDeleting: true,
enableAttachments: false,
enableHashtags: false,
enablePinging: false,
enableDeletingCommentWithReplies: false,
enableNavigation: true,
postCommentOnEnter: false,
forceResponsive: false,
readOnly: false,
defaultNavigationSortKey: 'newest',
// Colors
highlightColor: '#2793e6',
deleteButtonColor: '#C9302C',
scrollContainer: this.$el,
roundProfilePictures: false,
textareaRows: 2,
textareaRowsOnFocus: 2,
textareaMaxRows: 5,
maxRepliesVisible: 2,
fieldMappings: {
id: 'id',
parent: 'parent',
created: 'created',
modified: 'modified',
content: 'content',
file: 'file',
fileURL: 'file_url',
fileMimeType: 'file_mime_type',
pings: 'pings',
creator: 'creator',
fullname: 'fullname',
profileURL: 'profile_url',
profilePictureURL: 'profile_picture_url',
isNew: 'is_new',
createdByAdmin: 'created_by_admin',
createdByCurrentUser: 'created_by_current_user',
upvoteCount: 'upvote_count',
userHasUpvoted: 'user_has_upvoted'
},
getUsers: function(success, error) {success([])},
getComments: function(success, error) {success([])},
postComment: function(commentJSON, success, error) {success(commentJSON)},
putComment: function(commentJSON, success, error) {success(commentJSON)},
deleteComment: function(commentJSON, success, error) {success()},
upvoteComment: function(commentJSON, success, error) {success(commentJSON)},
hashtagClicked: function(hashtag) {},
pingClicked: function(userId) {},
uploadAttachments: function(commentArray, success, error) {success(commentArray)},
refresh: function() {},
timeFormatter: function(time) {return new Date(time).toLocaleDateString()}
}
},
// Initialization
// ==============
init: function(options, el) {
this.$el = $(el);
this.$el.addClass('jquery-comments');
this.undelegateEvents();
this.delegateEvents();
// Detect mobile devices
(function(a){(jQuery.browser=jQuery.browser||{}).mobile=/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))})(navigator.userAgent||navigator.vendor||window.opera);
if($.browser.mobile) this.$el.addClass('mobile');
// Init options
this.options = $.extend(true, {}, this.getDefaultOptions(), options);;
// Read-only mode
if(this.options.readOnly) this.$el.addClass('read-only');
// Set initial sort key
this.currentSortKey = this.options.defaultNavigationSortKey;
// Create CSS declarations for highlight color
this.createCssDeclarations();
// Fetching data and rendering
this.fetchDataAndRender();
},
delegateEvents: function() {
this.bindEvents(false);
},
undelegateEvents: function() {
this.bindEvents(true);
},
bindEvents: function(unbind) {
var bindFunction = unbind ? 'off' : 'on';
for (var key in this.events) {
var eventName = key.split(' ')[0];
var selector = key.split(' ').slice(1).join(' ');
var methodNames = this.events[key].split(' ');
for(var index in methodNames) {
if(methodNames.hasOwnProperty(index)) {
var method = this[methodNames[index]];
// Keep the context
method = $.proxy(method, this);
if (selector == '') {
this.$el[bindFunction](eventName, method);
} else {
this.$el[bindFunction](eventName, selector, method);
}
}
}
}
},
// Basic functionalities
// =====================
fetchDataAndRender: function () {
var self = this;
this.commentsById = {};
this.usersById = {};
this.$el.empty();
this.createHTML();
// Render after data has been fetched
var dataFetched = this.after(this.options.enablePinging ? 2 : 1, function() {
self.dataFetched = true;
self.render();
});
// Comments
// ========
var commentsFetched = function(commentsArray) {
// Convert comments to custom data model
var commentModels = commentsArray.map(function(commentsJSON){
return self.createCommentModel(commentsJSON)
});
// Sort comments by date (oldest first so that they can be appended to the data model
// without caring dependencies)
self.sortComments(commentModels, 'oldest');
$(commentModels).each(function(index, commentModel) {
self.addCommentToDataModel(commentModel);
});
dataFetched();
};
this.options.getComments(commentsFetched, dataFetched);
// Users
// =====
if(this.options.enablePinging) {
var usersFetched = function(userArray) {
$(userArray).each(function(index, user) {
self.usersById[user.id] = user;
});
dataFetched();
}
this.options.getUsers(usersFetched, dataFetched);
}
},
fetchNext: function() {
var self = this;
// Loading indicator
var spinner = this.createSpinner();
this.$el.find('ul#comment-list').append(spinner);
var success = function (commentModels) {
$(commentModels).each(function(index, commentModel) {
self.createComment(commentModel);
});
spinner.remove();
}
var error = function() {
spinner.remove();
}
this.options.getComments(success, error);
},
createCommentModel: function(commentJSON) {
var commentModel = this.applyInternalMappings(commentJSON);
commentModel.childs = [];
return commentModel;
},
addCommentToDataModel: function(commentModel) {
if(!(commentModel.id in this.commentsById)) {
this.commentsById[commentModel.id] = commentModel;
// Update child array of the parent (append childs to the array of outer most parent)
if(commentModel.parent) {
var outermostParent = this.getOutermostParent(commentModel.parent);
outermostParent.childs.push(commentModel.id);
}
}
},
updateCommentModel: function(commentModel) {
$.extend(this.commentsById[commentModel.id], commentModel);
},
render: function() {
var self = this;
// Prevent re-rendering if data hasn't been fetched
if(!this.dataFetched) return;
// Show active container
this.showActiveContainer();
// Create comments
this.createComments();
// Create attachments if enabled
if(this.options.enableAttachments) this.createAttachments();
// Remove spinner
this.$el.find('> .spinner').remove();
this.options.refresh();
},
showActiveContainer: function() {
var activeNavigationEl = this.$el.find('.navigation li[data-container-name].active');
var containerName = activeNavigationEl.data('container-name');
var containerEl = this.$el.find('[data-container="' + containerName + '"]');
containerEl.siblings('[data-container]').hide();
containerEl.show();
},
createComments: function() {
var self = this;
// Create the list element before appending to DOM in order to reach better performance
this.$el.find('#comment-list').remove();
var commentList = $('
', {
id: 'comment-list',
'class': 'main'
});
// Divide commments into main level comments and replies
var mainLevelComments = [];
var replies = [];
$(this.getComments()).each(function(index, commentModel) {
if(commentModel.parent == null) {
mainLevelComments.push(commentModel);
} else {
replies.push(commentModel);
}
});
// Append main level comments
this.sortComments(mainLevelComments, this.currentSortKey);
mainLevelComments.reverse(); // Reverse the order as they are prepended to DOM
$(mainLevelComments).each(function(index, commentModel) {
self.addComment(commentModel, commentList);
});
// Append replies in chronological order
this.sortComments(replies, 'oldest');
$(replies).each(function(index, commentModel) {
self.addComment(commentModel, commentList);
});
// Appned list to DOM
this.$el.find('[data-container="comments"]').prepend(commentList);
},
createAttachments: function() {
var self = this;
// Create the list element before appending to DOM in order to reach better performance
this.$el.find('#attachment-list').remove();
var attachmentList = $('
', {
id: 'attachment-list',
'class': 'main'
});
var attachments = this.getAttachments();
this.sortComments(attachments, 'newest');
attachments.reverse(); // Reverse the order as they are prepended to DOM
$(attachments).each(function(index, commentModel) {
self.addAttachment(commentModel, attachmentList);
});
// Appned list to DOM
this.$el.find('[data-container="attachments"]').prepend(attachmentList);
},
addComment: function(commentModel, commentList) {
commentList = commentList || this.$el.find('#comment-list');
var commentEl = this.createCommentElement(commentModel);
// Case: reply
if(commentModel.parent) {
var directParentEl = commentList.find('.comment[data-id="'+commentModel.parent+'"]');
// Re-render action bar of direct parent element
this.reRenderCommentActionBar(commentModel.parent);
// Force replies into one level only
var outerMostParent = directParentEl.parents('.comment').last();
if(outerMostParent.length == 0) outerMostParent = directParentEl;
// Append element to DOM
var childCommentsEl = outerMostParent.find('.child-comments');
var commentingField = childCommentsEl.find('.commenting-field');
if(commentingField.length) {
commentingField.before(commentEl)
} else {
childCommentsEl.append(commentEl);
}
// Update toggle all -button
this.updateToggleAllButton(outerMostParent);
// Case: main level comment
} else {
commentList.prepend(commentEl);
}
},
addAttachment: function(commentModel, commentList) {
commentList = commentList || this.$el.find('#attachment-list');
var commentEl = this.createCommentElement(commentModel);
commentList.prepend(commentEl);
},
removeComment: function(commentId) {
var self = this;
var commentModel = this.commentsById[commentId];
// Remove child comments recursively
var childComments = this.getChildComments(commentModel.id);
$(childComments).each(function(index, childComment) {
self.removeComment(childComment.id);
});
// Update the child array of outermost parent
if(commentModel.parent) {
var outermostParent = this.getOutermostParent(commentModel.parent);
var indexToRemove = outermostParent.childs.indexOf(commentModel.id);
outermostParent.childs.splice(indexToRemove, 1);
}
// Remove the comment from data model
delete this.commentsById[commentId];
var commentElements = this.$el.find('li.comment[data-id="'+commentId+'"]');
var parentEl = commentElements.parents('li.comment').last();
// Remove the element
commentElements.remove();
// Update the toggle all button
this.updateToggleAllButton(parentEl);
},
uploadAttachments: function(files, commentingField) {
var self = this;
if(!commentingField) commentingField = this.$el.find('.commenting-field.main');
var uploadButton = commentingField.find('.upload');
var isReply = !commentingField.hasClass('main');
var fileCount = files.length;
if(fileCount) {
var textarea = commentingField.find('.textarea');
// Disable upload button and append spinners while request is pending
uploadButton.removeClass('enabled');
var attachmentListSpinner = this.createSpinner();
var commentListSpinner = this.createSpinner();
this.$el.find('ul#attachment-list').prepend(attachmentListSpinner);
if(isReply) {
commentingField.before(commentListSpinner);
} else {
this.$el.find('ul#comment-list').prepend(commentListSpinner);
}
var success = function(commentArray) {
$(commentArray).each(function(index, commentJSON) {
var commentModel = self.createCommentModel(commentJSON);
self.addCommentToDataModel(commentModel);
self.addComment(commentModel);
self.addAttachment(commentModel);
});
// Close the commenting field if all the uploads were successfull
// and there's no content besides the attachment
if(commentArray.length == fileCount && self.getTextareaContent(textarea).length == 0) {
commentingField.find('.close').trigger('click');
}
// Enable upload button and remove spinners
uploadButton.addClass('enabled');
commentListSpinner.remove();
attachmentListSpinner.remove();
};
var error = function() {
// Enable upload button and remove spinners
uploadButton.addClass('enabled');
commentListSpinner.remove();
attachmentListSpinner.remove();
};
var commentArray = [];
$(files).each(function(index, file) {
// Create comment JSON
var commentJSON = self.createCommentJSON(textarea);
commentJSON.id += '-' + index;
commentJSON.content = '';
commentJSON.file = file;
commentJSON.fileURL = 'C:/fakepath/' + file.name;
commentJSON.fileMimeType = file.type;
// Reverse mapping
commentJSON = self.applyExternalMappings(commentJSON);
commentArray.push(commentJSON);
});
self.options.uploadAttachments(commentArray, success, error);
}
// Clear the input field
uploadButton.find('input').val('');
},
updateToggleAllButton: function(parentEl) {
// Don't hide replies if maxRepliesVisible is null or undefined
if (this.options.maxRepliesVisible == null) return;
var childCommentsEl = parentEl.find('.child-comments');
var childComments = childCommentsEl.find('.comment');
var toggleAllButton = childCommentsEl.find('li.toggle-all');
childComments.removeClass('hidden-reply');
// Select replies to be hidden
if (this.options.maxRepliesVisible === 0) {
var hiddenReplies = childComments;
} else {
var hiddenReplies = childComments.slice(0, -this.options.maxRepliesVisible);
}
// Add identifying class for hidden replies so they can be toggled
hiddenReplies.addClass('hidden-reply');
// Show all replies if replies are expanded
if(toggleAllButton.find('span.text').text() == this.options.textFormatter(this.options.hideRepliesText)) {
hiddenReplies.addClass('visible');
}
// Make sure that toggle all button is present
if(childComments.length > this.options.maxRepliesVisible) {
// Append button to toggle all replies if necessary
if(!toggleAllButton.length) {
toggleAllButton = $('', {
'class': 'toggle-all highlight-font-bold'
});
var toggleAllButtonText = $('', {
'class': 'text'
});
var caret = $('', {
'class': 'caret'
});
// Append toggle button to DOM
toggleAllButton.append(toggleAllButtonText).append(caret);
childCommentsEl.prepend(toggleAllButton);
}
// Update the text of toggle all -button
this.setToggleAllButtonText(toggleAllButton, false);
// Make sure that toggle all button is not present
} else {
toggleAllButton.remove();
}
},
sortComments: function (comments, sortKey) {
var self = this;
// Sort by popularity
if(sortKey == 'popularity') {
comments.sort(function(commentA, commentB) {
var pointsOfA = commentA.childs.length;
var pointsOfB = commentB.childs.length;
if(self.options.enableUpvoting) {
pointsOfA += commentA.upvoteCount;
pointsOfB += commentB.upvoteCount;
}
if(pointsOfB != pointsOfA) {
return pointsOfB - pointsOfA;
} else {
// Return newer if popularity is the same
var createdA = new Date(commentA.created).getTime();
var createdB = new Date(commentB.created).getTime();
return createdB - createdA;
}
});
// Sort by date
} else {
comments.sort(function(commentA, commentB) {
var createdA = new Date(commentA.created).getTime();
var createdB = new Date(commentB.created).getTime();
if(sortKey == 'oldest') {
return createdA - createdB;
} else {
return createdB - createdA;
}
});
}
},
sortAndReArrangeComments: function(sortKey) {
var commentList = this.$el.find('#comment-list');
// Get main level comments
var mainLevelComments = this.getComments().filter(function(commentModel){return !commentModel.parent});
this.sortComments(mainLevelComments, sortKey);
// Rearrange the main level comments
$(mainLevelComments).each(function(index, commentModel) {
var commentEl = commentList.find('> li.comment[data-id='+commentModel.id+']');
commentList.append(commentEl);
});
},
showActiveSort: function() {
var activeElements = this.$el.find('.navigation li[data-sort-key="' + this.currentSortKey + '"]');
// Indicate active sort
this.$el.find('.navigation li').removeClass('active');
activeElements.addClass('active');
// Update title for dropdown
var titleEl = this.$el.find('.navigation .title');
if(this.currentSortKey != 'attachments') {
titleEl.addClass('active');
titleEl.find('header').html(activeElements.first().html());
} else {
var defaultDropdownEl = this.$el.find('.navigation ul.dropdown').children().first();
titleEl.find('header').html(defaultDropdownEl.html());
}
// Show active container
this.showActiveContainer();
},
forceResponsive: function() {
this.$el.addClass('responsive');
},
// Event handlers
// ==============
closeDropdowns: function() {
this.$el.find('.dropdown').hide();
},
saveOnKeydown: function(ev) {
// Save comment on cmd/ctrl + enter
if(ev.keyCode == 13) {
var metaKey = ev.metaKey || ev.ctrlKey;
if(this.options.postCommentOnEnter || metaKey) {
var el = $(ev.currentTarget);
el.siblings('.control-row').find('.save').trigger('click');
ev.stopPropagation();
ev.preventDefault();
}
}
},
saveEditableContent: function(ev) {
var el = $(ev.currentTarget);
el.data('before', el.html());
},
checkEditableContentForChange: function(ev) {
var el = $(ev.currentTarget);
if (el.data('before') != el.html()) {
el.data('before', el.html());
el.trigger('change');
}
},
navigationElementClicked: function(ev) {
var navigationEl = $(ev.currentTarget);
var sortKey = navigationEl.data().sortKey;
// Sort the comments if necessary
if(sortKey != 'attachments') {
this.sortAndReArrangeComments(sortKey);
}
// Save the current sort key
this.currentSortKey = sortKey;
this.showActiveSort();
},
toggleNavigationDropdown: function(ev) {
// Prevent closing immediately
ev.stopPropagation();
var dropdown = $(ev.currentTarget).find('~ .dropdown');
dropdown.toggle();
},
showMainCommentingField: function(ev) {
var mainTextarea = $(ev.currentTarget);
mainTextarea.siblings('.control-row').show();
mainTextarea.parent().find('.close').show();
mainTextarea.parent().find('.upload.inline-button').hide();
mainTextarea.focus();
},
hideMainCommentingField: function(ev) {
var closeButton = $(ev.currentTarget);
var mainTextarea = this.$el.find('.commenting-field.main .textarea');
var mainControlRow = this.$el.find('.commenting-field.main .control-row');
this.clearTextarea(mainTextarea);
this.adjustTextareaHeight(mainTextarea, false);
mainControlRow.hide();
closeButton.hide();
mainTextarea.parent().find('.upload.inline-button').show();
mainTextarea.blur();
},
increaseTextareaHeight: function(ev) {
var textarea = $(ev.currentTarget);
this.adjustTextareaHeight(textarea, true);
},
textareaContentChanged: function(ev) {
var textarea = $(ev.currentTarget);
var saveButton = textarea.siblings('.control-row').find('.save');
// Update parent id if reply-to tag was removed
if(!textarea.find('.reply-to.tag').length) {
var commentId = textarea.attr('data-comment');
// Case: editing comment
if(commentId) {
var parentComments = textarea.parents('li.comment');
if(parentComments.length > 1) {
var parentId = parentComments.last().data('id');
textarea.attr('data-parent', parentId);
}
// Case: new comment
} else {
var parentId = textarea.parents('li.comment').last().data('id');
textarea.attr('data-parent', parentId);
}
}
// Move close button if scrollbar is visible
var commentingField = textarea.parents('.commenting-field').first();
if(textarea[0].scrollHeight > textarea.outerHeight()) {
commentingField.addClass('scrollable');
} else {
commentingField.removeClass('scrollable');
}
// Check if content or parent has changed if editing
var contentOrParentChangedIfEditing = true;
var content = this.getTextareaContent(textarea, true);
if(commentId = textarea.attr('data-comment')) {
var contentChanged = content != this.commentsById[commentId].content;
var parentFromModel;
if(this.commentsById[commentId].parent) {
parentFromModel = this.commentsById[commentId].parent.toString();
}
var parentChanged = textarea.attr('data-parent') != parentFromModel;
contentOrParentChangedIfEditing = contentChanged || parentChanged;
}
// Check whether save button needs to be enabled
if(content.length && contentOrParentChangedIfEditing) {
saveButton.addClass('enabled');
} else {
saveButton.removeClass('enabled');
}
},
removeCommentingField: function(ev) {
var closeButton = $(ev.currentTarget);
// Remove edit class from comment if user was editing the comment
var textarea = closeButton.siblings('.textarea');
if(textarea.attr('data-comment')) {
closeButton.parents('li.comment').first().removeClass('edit');
}
// Remove the field
var commentingField = closeButton.parents('.commenting-field').first();
commentingField.remove();
},
postComment: function(ev) {
var self = this;
var sendButton = $(ev.currentTarget);
var commentingField = sendButton.parents('.commenting-field').first();
var textarea = commentingField.find('.textarea');
// Disable send button while request is pending
sendButton.removeClass('enabled');
// Create comment JSON
var commentJSON = this.createCommentJSON(textarea);
// Reverse mapping
commentJSON = this.applyExternalMappings(commentJSON);
var success = function(commentJSON) {
self.createComment(commentJSON);
commentingField.find('.close').trigger('click');
};
var error = function() {
sendButton.addClass('enabled');
};
this.options.postComment(commentJSON, success, error);
},
createComment: function(commentJSON) {
var commentModel = this.createCommentModel(commentJSON);
this.addCommentToDataModel(commentModel);
this.addComment(commentModel);
},
putComment: function(ev) {
var self = this;
var saveButton = $(ev.currentTarget);
var commentingField = saveButton.parents('.commenting-field').first();
var textarea = commentingField.find('.textarea');
// Disable send button while request is pending
saveButton.removeClass('enabled');
// Use a clone of the existing model and update the model after succesfull update
var commentJSON = $.extend({}, this.commentsById[textarea.attr('data-comment')]);
$.extend(commentJSON, {
parent: textarea.attr('data-parent') || null,
content: this.getTextareaContent(textarea),
pings: this.getPings(textarea),
modified: new Date().getTime()
});
// Reverse mapping
commentJSON = this.applyExternalMappings(commentJSON);
var success = function(commentJSON) {
// The outermost parent can not be changed by editing the comment so the childs array
// of parent does not require an update
var commentModel = self.createCommentModel(commentJSON);
// Delete childs array from new comment model since it doesn't need an update
delete commentModel['childs'];
self.updateCommentModel(commentModel);
// Close the editing field
commentingField.find('.close').trigger('click');
// Re-render the comment
self.reRenderComment(commentModel.id);
};
var error = function() {
saveButton.addClass('enabled');
};
this.options.putComment(commentJSON, success, error);
},
deleteComment: function(ev) {
var self = this;
var deleteButton = $(ev.currentTarget);
var commentEl = deleteButton.parents('.comment').first();
var commentJSON = $.extend({}, this.commentsById[commentEl.attr('data-id')]);
var commentId = commentJSON.id;
var parentId = commentJSON.parent;
// Disable send button while request is pending
deleteButton.removeClass('enabled');
// Reverse mapping
commentJSON = this.applyExternalMappings(commentJSON);
var success = function() {
self.removeComment(commentId);
if(parentId) self.reRenderCommentActionBar(parentId);
};
var error = function() {
deleteButton.addClass('enabled');
};
this.options.deleteComment(commentJSON, success, error);
},
hashtagClicked: function(ev) {
var el = $(ev.currentTarget);
var value = el.attr('data-value');
this.options.hashtagClicked(value);
},
pingClicked: function(ev) {
var el = $(ev.currentTarget);
var value = el.attr('data-value');
this.options.pingClicked(value);
},
fileInputChanged: function(ev, files) {
var files = ev.currentTarget.files;
var commentingField = $(ev.currentTarget).parents('.commenting-field').first();
this.uploadAttachments(files, commentingField);
},
upvoteComment: function(ev) {
var self = this;
var commentEl = $(ev.currentTarget).parents('li.comment').first();
var commentModel = commentEl.data().model;
// Check whether user upvoted the comment or revoked the upvote
var previousUpvoteCount = commentModel.upvoteCount;
var newUpvoteCount;
if(commentModel.userHasUpvoted) {
newUpvoteCount = previousUpvoteCount - 1;
} else {
newUpvoteCount = previousUpvoteCount + 1;
}
// Show changes immediatelly
commentModel.userHasUpvoted = !commentModel.userHasUpvoted;
commentModel.upvoteCount = newUpvoteCount;
this.reRenderUpvotes(commentModel.id);
// Reverse mapping
var commentJSON = $.extend({}, commentModel);
commentJSON = this.applyExternalMappings(commentJSON);
var success = function(commentJSON) {
var commentModel = self.createCommentModel(commentJSON);
self.updateCommentModel(commentModel);
self.reRenderUpvotes(commentModel.id);
};
var error = function() {
// Revert changes
commentModel.userHasUpvoted = !commentModel.userHasUpvoted;
commentModel.upvoteCount = previousUpvoteCount;
self.reRenderUpvotes(commentModel.id);
};
this.options.upvoteComment(commentJSON, success, error);
},
toggleReplies: function(ev) {
var el = $(ev.currentTarget);
el.siblings('.hidden-reply').toggleClass('visible');
this.setToggleAllButtonText(el, true);
},
replyButtonClicked: function(ev) {
var replyButton = $(ev.currentTarget);
var outermostParent = replyButton.parents('li.comment').last();
var parentId = replyButton.parents('.comment').first().data().id;
// Remove existing field
var replyField = outermostParent.find('.child-comments > .commenting-field');
if(replyField.length) replyField.remove();
var previousParentId = replyField.find('.textarea').attr('data-parent');
// Create the reply field (do not re-create)
if(previousParentId != parentId) {
replyField = this.createCommentingFieldElement(parentId);
outermostParent.find('.child-comments').append(replyField);
// Move cursor to end
var textarea = replyField.find('.textarea');
this.moveCursorToEnd(textarea)
// Make sure the reply field will be displayed
var scrollTop = this.options.scrollContainer.scrollTop();
var endOfReply = scrollTop + replyField.position().top + replyField.outerHeight();
var endOfScrollable = scrollTop + this.options.scrollContainer.outerHeight();
if(endOfReply > endOfScrollable) {
var newScrollTop = scrollTop + (endOfReply - endOfScrollable);
this.options.scrollContainer.scrollTop(newScrollTop);
}
}
},
editButtonClicked: function(ev) {
var editButton = $(ev.currentTarget);
var commentEl = editButton.parents('li.comment').first();
var commentModel = commentEl.data().model;
commentEl.addClass('edit');
// Create the editing field
var editField = this.createCommentingFieldElement(commentModel.parent, commentModel.id);
commentEl.find('.comment-wrapper').first().append(editField);
// Append original content
var textarea = editField.find('.textarea');
textarea.attr('data-comment', commentModel.id);
// Escaping HTML
textarea.append(this.getFormattedCommentContent(commentModel, true));
// Move cursor to end
this.moveCursorToEnd(textarea);
},
showDroppableOverlay: function(ev) {
if(this.options.enableAttachments) {
this.$el.find('.droppable-overlay').css('top', this.$el[0].scrollTop);
this.$el.find('.droppable-overlay').show();
this.$el.addClass('drag-ongoing');
}
},
handleDragEnter: function(ev) {
var count = $(ev.currentTarget).data('dnd-count') || 0;
count++;
$(ev.currentTarget).data('dnd-count', count);
$(ev.currentTarget).addClass('drag-over');
},
handleDragLeave: function(ev, callback) {
var count = $(ev.currentTarget).data('dnd-count');
count--;
$(ev.currentTarget).data('dnd-count', count);
if(count == 0) {
$(ev.currentTarget).removeClass('drag-over');
if(callback) callback();
}
},
handleDragLeaveForOverlay: function(ev) {
var self = this;
this.handleDragLeave(ev, function() {
self.hideDroppableOverlay();
});
},
handleDragLeaveForDroppable: function(ev) {
this.handleDragLeave(ev);
},
handleDragOverForOverlay: function(ev) {
ev.stopPropagation();
ev.preventDefault();
ev.originalEvent.dataTransfer.dropEffect = 'copy';
},
hideDroppableOverlay: function() {
this.$el.find('.droppable-overlay').hide();
this.$el.removeClass('drag-ongoing');
},
handleDrop: function(ev) {
ev.preventDefault();
// Reset DND counts
$(ev.target).trigger('dragleave');
// Hide the overlay and upload the files
this.hideDroppableOverlay();
this.uploadAttachments(ev.originalEvent.dataTransfer.files);
},
stopPropagation: function(ev) {
ev.stopPropagation();
},
// HTML elements
// =============
createHTML: function() {
var self = this;
// Commenting field
var mainCommentingField = this.createMainCommentingFieldElement();
this.$el.append(mainCommentingField);
// Hide control row and close button
var mainControlRow = mainCommentingField.find('.control-row');
mainControlRow.hide();
mainCommentingField.find('.close').hide();
// Navigation bar
if (this.options.enableNavigation) {
this.$el.append(this.createNavigationElement());
this.showActiveSort();
}
// Loading spinner
var spinner = this.createSpinner();
this.$el.append(spinner);
// Comments container
var commentsContainer = $('', {
'class': 'data-container',
'data-container': 'comments'
});
this.$el.append(commentsContainer);
// "No comments" placeholder
var noComments = $('', {
'class': 'no-comments no-data',
text: this.options.textFormatter(this.options.noCommentsText)
});
var noCommentsIcon = $('', {
'class': 'fa fa-comments fa-2x'
});
if(this.options.noCommentsIconURL.length) {
noCommentsIcon.css('background-image', 'url("'+this.options.noCommentsIconURL+'")');
noCommentsIcon.addClass('image');
}
noComments.prepend($(' ')).prepend(noCommentsIcon);
commentsContainer.append(noComments);
// Attachments
if(this.options.enableAttachments) {
// Attachments container
var attachmentsContainer = $('', {
'class': 'data-container',
'data-container': 'attachments'
});
this.$el.append(attachmentsContainer);
// "No attachments" placeholder
var noAttachments = $('', {
'class': 'no-attachments no-data',
text: this.options.textFormatter(this.options.noAttachmentsText)
});
var noAttachmentsIcon = $('', {
'class': 'fa fa-paperclip fa-2x'
});
if(this.options.attachmentIconURL.length) {
noAttachmentsIcon.css('background-image', 'url("'+this.options.attachmentIconURL+'")');
noAttachmentsIcon.addClass('image');
}
noAttachments.prepend($(' ')).prepend(noAttachmentsIcon);
attachmentsContainer.append(noAttachments);
// Drag & dropping attachments
var droppableOverlay = $('', {
'class': 'droppable-overlay'
});
var droppableContainer = $('', {
'class': 'droppable-container'
});
var droppable = $('', {
'class': 'droppable'
});
var uploadIcon = $('', {
'class': 'fa fa-paperclip fa-4x'
});
if(this.options.uploadIconURL.length) {
uploadIcon.css('background-image', 'url("'+this.options.uploadIconURL+'")');
uploadIcon.addClass('image');
}
var dropAttachmentText = $('', {
text: this.options.textFormatter(this.options.attachmentDropText)
});
droppable.append(uploadIcon);
droppable.append(dropAttachmentText);
droppableOverlay.html(droppableContainer.html(droppable)).hide();
this.$el.append(droppableOverlay);
}
},
createProfilePictureElement: function(src) {
if(src) {
var profilePicture = $('', {
src: src
});
} else {
var profilePicture = $('', {
'class': 'fa fa-user'
});
}
profilePicture.addClass('profile-picture');
if(this.options.roundProfilePictures) profilePicture.addClass('round');
return profilePicture;
},
createMainCommentingFieldElement: function() {
return this.createCommentingFieldElement(undefined, undefined, true);
},
createCommentingFieldElement: function(parentId, existingCommentId, isMain) {
var self = this;
// Commenting field
var commentingField = $('', {
'class': 'commenting-field'
});
if(isMain) commentingField.addClass('main');
// Profile picture
if(existingCommentId) {
var profilePictureURL = this.commentsById[existingCommentId].profilePictureURL;
} else {
var profilePictureURL = this.options.profilePictureURL;
}
var profilePicture = this.createProfilePictureElement(profilePictureURL);
// New comment
var textareaWrapper = $('', {
'class': 'textarea-wrapper'
});
// Control row
var controlRow = $('', {
'class': 'control-row'
});
// Textarea
var textarea = $('', {
'class': 'textarea',
'data-placeholder': this.options.textFormatter(this.options.textareaPlaceholderText),
contenteditable: true
});
// Setting the initial height for the textarea
this.adjustTextareaHeight(textarea, false);
// Close button
var closeButton = $('', {
'class': 'close inline-button'
}).append($(''));
// Save button text
if(existingCommentId) {
var saveButtonText = this.options.textFormatter(this.options.saveText);
// Delete button
var deleteButton = $('', {
'class': 'delete',
text: this.options.textFormatter(this.options.deleteText)
}).css('background-color', this.options.deleteButtonColor);
controlRow.append(deleteButton);
// Enable the delete button only if the user is allowed to delete
if(this.isAllowedToDelete(existingCommentId)) deleteButton.addClass('enabled')
} else {
var saveButtonText = this.options.textFormatter(this.options.sendText);
// Add upload button if attachments are enabled
if(this.options.enableAttachments) {
var uploadButton = $('', {
'class': 'enabled upload'
});
var uploadIcon = $('', {
'class': 'fa fa-paperclip'
});
var fileInput = $('', {
type: 'file',
'data-role': 'none' // Prevent jquery-mobile for adding classes
});
// Multi file upload might not work with backend as the the file names
// may be the same causing duplicates
//if(!$.browser.mobile) fileInput.attr('multiple', 'multiple');
if(this.options.uploadIconURL.length) {
uploadIcon.css('background-image', 'url("'+this.options.uploadIconURL+'")');
uploadIcon.addClass('image');
}
uploadButton.append(uploadIcon).append(fileInput);
// Main upload button
controlRow.append(uploadButton.clone());
// Inline upload button for main commenting field
if(isMain) {
textareaWrapper.append(uploadButton.clone().addClass('inline-button'));
}
}
}
// Save button
var saveButtonClass = existingCommentId ? 'update' : 'send';
var saveButton = $('', {
'class': saveButtonClass + ' save highlight-background',
text: saveButtonText
});
// Populate the element
controlRow.prepend(saveButton);
textareaWrapper.append(closeButton).append(textarea).append(controlRow);
commentingField.append(profilePicture).append(textareaWrapper);
if(parentId) {
// Set the parent id to the field if necessary
textarea.attr('data-parent', parentId);
// Append reply-to tag if necessary
var parentModel = this.commentsById[parentId];
if(parentModel.parent) {
textarea.html(' '); // Needed to set the cursor to correct place
// Creating the reply-to tag
var replyToName = '@' + parentModel.fullname;
var replyToTag = this.createTagElement(replyToName, 'reply-to', parentModel.creator);
textarea.prepend(replyToTag);
}
}
// Pinging users
if(this.options.enablePinging) {
textarea.textcomplete([{
match: /(^|\s)@(([a-zäöüß]|\s)*)$/im,
search: function (term, callback) {
term = self.normalizeSpaces(term);
// Users excluding self and already pinged users
var pings = self.getPings(textarea);
var users = self.getUsers().filter(function(user) {
var isSelf = user.id == self.options.currentUserId;
var alreadyPinged = pings.indexOf(user.id) != -1;
return !isSelf && !alreadyPinged;
});
// Sort users alphabetically
users.sort(function(a,b) {
var nameA = a.fullname.toLowerCase().trim();
var nameB = b.fullname.toLowerCase().trim();
if(nameA < nameB) return -1;
if(nameA > nameB) return 1;
return 0;
});
// Filter users by search term
callback($.map(users, function (user) {
var wordsInSearchTerm = term.split(' ');
var wordsInName = user.fullname.split(' ');
// Loop all words in search term and ensure that they are found in the words of the fullname
var allSearchWordsFound = true;
$(wordsInSearchTerm).each(function(index, searchWord) {
var trimmedSearchWord = searchWord.toLowerCase().trim();
var trimmedSearchWordFound = false;
// Loop all words in the name and ensure that at least one of those starts with the search word
$(wordsInName).each(function(index, wordInName) {
var trimmedWordInName = wordInName.toLowerCase().trim();
if(trimmedWordInName.indexOf(trimmedSearchWord) == 0) trimmedSearchWordFound = true;
});
// Mark search as failed if even one search word was not found in the name
if(!trimmedSearchWordFound) allSearchWordsFound = false;
});
return allSearchWordsFound ? user : null;
}));
},
template: function(user) {
var wrapper = $('');
var profilePictureEl = $('', {
src: user.profile_picture_url,
'class': 'profile-picture round'
});
var detailsEl = $('', {
'class': 'details',
});
var nameEl = $('', {
'class': 'name',
}).html(user.fullname);
var emailEl = $('', {
'class': 'email',
}).html(user.email);
if (user.email) {
detailsEl.append(nameEl).append(emailEl);
} else {
detailsEl.addClass('no-email')
detailsEl.append(nameEl)
}
wrapper.append(profilePictureEl).append(detailsEl);
return wrapper.html();
},
replace: function (user) {
var tag = self.createTagElement('@' + user.fullname, 'ping', user.id);
return ' ' + tag[0].outerHTML + ' ';
},
}], {
appendTo: '.jquery-comments',
dropdownClassName: 'dropdown autocomplete',
maxCount: 5,
rightEdgeOffset: 0,
});
// OVERIDE TEXTCOMPLETE DROPDOWN POSITIONING
$.fn.textcomplete.Dropdown.prototype.render = function(zippedData) {
var contentsHtml = this._buildContents(zippedData);
var unzippedData = $.map(zippedData, function (d) { return d.value; });
if (zippedData.length) {
var strategy = zippedData[0].strategy;
if (strategy.id) {
this.$el.attr('data-strategy', strategy.id);
} else {
this.$el.removeAttr('data-strategy');
}
this._renderHeader(unzippedData);
this._renderFooter(unzippedData);
if (contentsHtml) {
this._renderContents(contentsHtml);
this._fitToBottom();
this._fitToRight();
this._activateIndexedItem();
}
this._setScroll();
} else if (this.noResultsMessage) {
this._renderNoResultsMessage(unzippedData);
} else if (this.shown) {
this.deactivate();
}
// CUSTOM CODE
// ===========
// Adjust vertical position
var top = parseInt(this.$el.css('top')) + self.options.scrollContainer.scrollTop();
this.$el.css('top', top);
// Adjust horizontal position
var originalLeft = this.$el.css('left');
this.$el.css('left', 0); // Left must be set to 0 in order to get the real width of the el
var maxLeft = self.$el.width() - this.$el.outerWidth();
var left = Math.min(maxLeft, parseInt(originalLeft));
this.$el.css('left', left);
// ===========
}
}
return commentingField;
},
createNavigationElement: function() {
var navigationEl = $('