Files
wyczarujprezent.pl/modules/jxblogcomment/views/js/jquery-comments-modified.js
2024-10-28 22:14:22 +01:00

2367 lines
91 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = $('<ul/>', {
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 = $('<ul/>', {
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 = $('<li/>', {
'class': 'toggle-all highlight-font-bold'
});
var toggleAllButtonText = $('<span/>', {
'class': 'text'
});
var caret = $('<span/>', {
'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 = $('<div/>', {
'class': 'data-container',
'data-container': 'comments'
});
this.$el.append(commentsContainer);
// "No comments" placeholder
var noComments = $('<div/>', {
'class': 'no-comments no-data',
text: this.options.textFormatter(this.options.noCommentsText)
});
var noCommentsIcon = $('<i/>', {
'class': 'fa fa-comments fa-2x'
});
if(this.options.noCommentsIconURL.length) {
noCommentsIcon.css('background-image', 'url("'+this.options.noCommentsIconURL+'")');
noCommentsIcon.addClass('image');
}
noComments.prepend($('<br/>')).prepend(noCommentsIcon);
commentsContainer.append(noComments);
// Attachments
if(this.options.enableAttachments) {
// Attachments container
var attachmentsContainer = $('<div/>', {
'class': 'data-container',
'data-container': 'attachments'
});
this.$el.append(attachmentsContainer);
// "No attachments" placeholder
var noAttachments = $('<div/>', {
'class': 'no-attachments no-data',
text: this.options.textFormatter(this.options.noAttachmentsText)
});
var noAttachmentsIcon = $('<i/>', {
'class': 'fa fa-paperclip fa-2x'
});
if(this.options.attachmentIconURL.length) {
noAttachmentsIcon.css('background-image', 'url("'+this.options.attachmentIconURL+'")');
noAttachmentsIcon.addClass('image');
}
noAttachments.prepend($('<br/>')).prepend(noAttachmentsIcon);
attachmentsContainer.append(noAttachments);
// Drag & dropping attachments
var droppableOverlay = $('<div/>', {
'class': 'droppable-overlay'
});
var droppableContainer = $('<div/>', {
'class': 'droppable-container'
});
var droppable = $('<div/>', {
'class': 'droppable'
});
var uploadIcon = $('<i/>', {
'class': 'fa fa-paperclip fa-4x'
});
if(this.options.uploadIconURL.length) {
uploadIcon.css('background-image', 'url("'+this.options.uploadIconURL+'")');
uploadIcon.addClass('image');
}
var dropAttachmentText = $('<div/>', {
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 = $('<img/>', {
src: src
});
} else {
var profilePicture = $('<i/>', {
'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 = $('<div/>', {
'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 = $('<div/>', {
'class': 'textarea-wrapper'
});
// Control row
var controlRow = $('<div/>', {
'class': 'control-row'
});
// Textarea
var textarea = $('<div/>', {
'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 = $('<span/>', {
'class': 'close inline-button'
}).append($('<i class="fa fa-window-close" aria-hidden="true"/>'));
// Save button text
if(existingCommentId) {
var saveButtonText = this.options.textFormatter(this.options.saveText);
// Delete button
var deleteButton = $('<span/>', {
'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 = $('<span/>', {
'class': 'enabled upload'
});
var uploadIcon = $('<i/>', {
'class': 'fa fa-paperclip'
});
var fileInput = $('<input/>', {
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 = $('<span/>', {
'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('&nbsp;'); // 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 = $('<div/>');
var profilePictureEl = $('<img/>', {
src: user.profile_picture_url,
'class': 'profile-picture round'
});
var detailsEl = $('<div/>', {
'class': 'details',
});
var nameEl = $('<div/>', {
'class': 'name',
}).html(user.fullname);
var emailEl = $('<div/>', {
'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 = $('<ul/>', {
'class': 'navigation'
});
var navigationWrapper = $('<div/>', {
'class': 'navigation-wrapper'
});
navigationEl.append(navigationWrapper);
// Newest
var newest = $('<li/>', {
text: this.options.textFormatter(this.options.newestText),
'data-sort-key': 'newest',
'data-container-name': 'comments'
});
// Oldest
var oldest = $('<li/>', {
text: this.options.textFormatter(this.options.oldestText),
'data-sort-key': 'oldest',
'data-container-name': 'comments'
});
// Popular
var popular = $('<li/>', {
text: this.options.textFormatter(this.options.popularText),
'data-sort-key': 'popularity',
'data-container-name': 'comments'
});
// Attachments
var attachments = $('<li/>', {
text: this.options.textFormatter(this.options.attachmentsText),
'data-sort-key': 'attachments',
'data-container-name': 'attachments'
});
// Attachments icon
var attachmentsIcon = $('<i/>', {
'class': 'fa fa-paperclip'
});
if(this.options.attachmentIconURL.length) {
attachmentsIcon.css('background-image', 'url("'+this.options.attachmentIconURL+'")');
attachmentsIcon.addClass('image');
}
attachments.prepend(attachmentsIcon);
// Responsive navigation
var dropdownNavigationWrapper = $('<div/>', {
'class': 'navigation-wrapper responsive'
});
var dropdownNavigation = $('<ul/>', {
'class': 'dropdown'
});
var dropdownTitle = $('<li/>', {
'class': 'title'
});
var dropdownTitleHeader = $('<header/>');
dropdownTitle.append(dropdownTitleHeader);
dropdownNavigationWrapper.append(dropdownTitle);
dropdownNavigationWrapper.append(dropdownNavigation);
navigationEl.append(dropdownNavigationWrapper);
// Populate elements
navigationWrapper.append(newest).append(oldest);
dropdownNavigation.append(newest.clone()).append(oldest.clone());
if(this.options.enableReplying || this.options.enableUpvoting) {
navigationWrapper.append(popular);
dropdownNavigation.append(popular.clone());
}
if(this.options.enableAttachments) {
navigationWrapper.append(attachments);
dropdownNavigationWrapper.append(attachments.clone());
}
if(this.options.forceResponsive) this.forceResponsive();
return navigationEl;
},
createSpinner: function() {
var spinner = $('<div/>', {
'class': 'spinner'
});
var spinnerIcon = $('<i/>', {
'class': 'fa fa-spinner fa-spin'
});
if(this.options.spinnerIconURL.length) {
spinnerIcon.css('background-image', 'url("'+this.options.spinnerIconURL+'")');
spinnerIcon.addClass('image');
}
spinner.html(spinnerIcon);
return spinner;
},
createCommentElement: function(commentModel) {
// Comment container element
var commentEl = $('<li/>', {
'data-id': commentModel.id,
'class': 'comment'
}).data('model', commentModel);
if(commentModel.createdByCurrentUser) commentEl.addClass('by-current-user');
if(commentModel.createdByAdmin) commentEl.addClass('by-admin');
// Child comments
var childComments = $('<ul/>', {
'class': 'child-comments'
});
// Comment wrapper
var commentWrapper = this.createCommentWrapperElement(commentModel);
commentEl.append(commentWrapper);
if(commentModel.parent == null) commentEl.append(childComments);
return commentEl;
},
createCommentWrapperElement: function(commentModel) {
var commentWrapper = $('<div/>', {
'class': 'comment-wrapper'
});
// Profile picture
var profilePicture = this.createProfilePictureElement(commentModel.profilePictureURL);
// Time
var time = $('<time/>', {
text: this.options.timeFormatter(commentModel.created),
'data-original': commentModel.created
});
// Name
var nameText = commentModel.createdByCurrentUser ? this.options.textFormatter(this.options.youText) : commentModel.fullname;
var name = $('<div/>', {
'class': 'name'
});
if(commentModel.profileURL) {
var link = $('<a/>', {
href: commentModel.profileURL,
text: nameText
});
name.html(link);
} else {
name.text(nameText);
}
// Highlight name for own comments and admin
if(commentModel.createdByCurrentUser || commentModel.createdByAdmin) name.addClass('highlight-font-bold');
// Show reply-to name if parent of parent exists
if(commentModel.parent) {
var parent = this.commentsById[commentModel.parent];
if(parent.parent) {
var replyTo = $('<span/>', {
'class': 'reply-to',
text: parent.fullname
});
// reply icon
var replyIcon = $('<i/>', {
'class': 'fa fa-share'
});
if(this.options.replyIconURL.length) {
replyIcon.css('background-image', 'url("'+this.options.replyIconURL+'")');
replyIcon.addClass('image');
}
replyTo.prepend(replyIcon);
name.append(replyTo);
}
}
// New tag
if(commentModel.isNew) {
var newTag = $('<span/>', {
'class': 'new highlight-background',
text: this.options.newText
});
name.append(newTag);
}
// Wrapper
var wrapper = $('<div/>', {
'class': 'wrapper'
});
// Content
var content = $('<div/>', {
'class': 'content'
});
// Case: attachment
var isAttachment = commentModel.fileURL != undefined;
if(isAttachment) {
var format = null;
var type = null;
// Type and format
if(commentModel.fileMimeType) {
var mimeTypeParts = commentModel.fileMimeType.split('/');
if(mimeTypeParts.length == 2) {
format = mimeTypeParts[1];
type = mimeTypeParts[0];
}
}
// Attachment link
var link = $('<a/>', {
'class': 'attachment',
href: commentModel.fileURL,
target: '_blank'
});
// Case: image preview
if(type == 'image') {
var image = $('<img/>', {
src: commentModel.fileURL
});
link.html(image);
// Case: video preview
} else if(type == 'video') {
var video = $('<video/>', {
src: commentModel.fileURL,
type: commentModel.fileMimeType,
controls: 'controls'
});
link.html(video);
// Case: icon and text
} else {
// Icon
var availableIcons = ['archive', 'audio', 'code', 'excel', 'image', 'movie', 'pdf', 'photo',
'picture', 'powerpoint', 'sound', 'video', 'word', 'zip'];
var iconClass = 'fa fa-file-o';
if(availableIcons.indexOf(format) > 0) {
iconClass = 'fa fa-file-' + format + '-o';
} else if(availableIcons.indexOf(type) > 0) {
iconClass = 'fa fa-file-' + type + '-o';
}
var fileIcon = $('<i/>', {
'class': iconClass
});
if(this.options.fileIconURL.length) {
fileIcon.css('background-image', 'url("'+this.options.fileIconURL+'")');
fileIcon.addClass('image');
}
// File name
var parts = commentModel.fileURL.split('/');
var fileName = parts[parts.length - 1];
fileName = fileName.split('?')[0];
fileName = decodeURIComponent(fileName);
link.text(fileName);
link.prepend(fileIcon);
}
content.html(link);
// Case: regular comment
} else {
content.html(this.getFormattedCommentContent(commentModel));
}
// Edited timestamp
if(commentModel.modified && commentModel.modified != commentModel.created) {
var editedTime = this.options.timeFormatter(commentModel.modified);
var edited = $('<time/>', {
'class': 'edited',
text: this.options.textFormatter(this.options.editedText) + ' ' + editedTime,
'data-original': commentModel.modified
});
content.append(edited);
}
// Actions
var actions = $('<span/>', {
'class': 'actions'
});
// Separator
var separator = $('<span/>', {
'class': 'separator',
text: '·'
});
// Reply
var reply = $('<button/>', {
'class': 'action reply',
'type': 'button',
text: this.options.textFormatter(this.options.replyText)
});
// Upvote icon
var upvoteIcon = $('<i/>', {
'class': 'fa fa-thumbs-up'
});
if(this.options.upvoteIconURL.length) {
upvoteIcon.css('background-image', 'url("'+this.options.upvoteIconURL+'")');
upvoteIcon.addClass('image');
}
// Upvotes
var upvotes = this.createUpvoteElement(commentModel);
// Append buttons for actions that are enabled
if(this.options.enableReplying) actions.append(reply);
if(this.options.enableUpvoting) actions.append(upvotes);
if(commentModel.createdByCurrentUser || this.options.currentUserIsAdmin) {
// Case: delete button for attachment
if(isAttachment && this.isAllowedToDelete(commentModel.id)) {
var deleteButton = $('<button/>', {
'class': 'action delete enabled',
text: this.options.textFormatter(this.options.deleteText)
});
actions.append(deleteButton);
// Case: edit button for regular comment
} else if(!isAttachment && this.options.enableEditing) {
var editButton = $('<button/>', {
'class': 'action edit',
text: this.options.textFormatter(this.options.editText)
});
actions.append(editButton);
}
}
// Append separators between the actions
actions.children().each(function(index, actionEl) {
if(!$(actionEl).is(':last-child')) {
$(actionEl).after(separator.clone());
}
});
wrapper.append(content);
wrapper.append(actions);
commentWrapper.append(profilePicture).append(time).append(name).append(wrapper);
return commentWrapper;
},
createUpvoteElement: function(commentModel) {
// Upvote icon
var upvoteIcon = $('<i/>', {
'class': 'fa fa-thumbs-up'
});
if(this.options.upvoteIconURL.length) {
upvoteIcon.css('background-image', 'url("'+this.options.upvoteIconURL+'")');
upvoteIcon.addClass('image');
}
// Upvotes
var upvoteEl = $('<button/>', {
'class': 'action upvote' + (commentModel.userHasUpvoted ? ' highlight-font' : '')
}).append($('<span/>', {
text: commentModel.upvoteCount,
'class': 'upvote-count'
})).append(upvoteIcon);
return upvoteEl;
},
createTagElement: function(text, extraClasses, value) {
var tagEl = $('<input/>', {
'class': 'tag',
'type': 'button',
'data-role': 'none',
});
if(extraClasses) tagEl.addClass(extraClasses);
tagEl.val(text);
tagEl.attr('data-value', value);
return tagEl;
},
reRenderComment: function(id) {
var commentModel = this.commentsById[id];
var commentElements = this.$el.find('li.comment[data-id="'+commentModel.id+'"]');
var self = this;
commentElements.each(function(index, commentEl) {
var commentWrapper = self.createCommentWrapperElement(commentModel);
$(commentEl).find('.comment-wrapper').first().replaceWith(commentWrapper);
});
},
reRenderCommentActionBar: function(id) {
var commentModel = this.commentsById[id];
var commentElements = this.$el.find('li.comment[data-id="'+commentModel.id+'"]');
var self = this;
commentElements.each(function(index, commentEl) {
var commentWrapper = self.createCommentWrapperElement(commentModel);
$(commentEl).find('.actions').first().replaceWith(commentWrapper.find('.actions'));
});
},
reRenderUpvotes: function(id) {
var commentModel = this.commentsById[id];
var commentElements = this.$el.find('li.comment[data-id="'+commentModel.id+'"]');
var self = this;
commentElements.each(function(index, commentEl) {
var upvotes = self.createUpvoteElement(commentModel);
$(commentEl).find('.upvote').first().replaceWith(upvotes);
});
},
// Styling
// =======
createCssDeclarations: function() {
// Remove previous css-declarations
$('head style.jquery-comments-css').remove();
// Navigation underline
this.createCss('.jquery-comments ul.navigation li.active:after {background: '
+ this.options.highlightColor + ' !important;',
+'}');
// Dropdown active element
this.createCss('.jquery-comments ul.navigation ul.dropdown li.active {background: '
+ this.options.highlightColor + ' !important;',
+'}');
// Background highlight
this.createCss('.jquery-comments .highlight-background {background: '
+ this.options.highlightColor + ' !important;',
+'}');
// Font highlight
this.createCss('.jquery-comments .highlight-font {color: '
+ this.options.highlightColor + ' !important;'
+'}');
this.createCss('.jquery-comments .highlight-font-bold {color: '
+ this.options.highlightColor + ' !important;'
+ 'font-weight: bold;'
+'}');
},
createCss: function(css) {
var styleEl = $('<style/>', {
type: 'text/css',
'class': 'jquery-comments-css',
text: css
});
$('head').append(styleEl);
},
// Utilities
// =========
getComments: function() {
var self = this;
return Object.keys(this.commentsById).map(function(id){return self.commentsById[id]});
},
getUsers: function() {
var self = this;
return Object.keys(this.usersById).map(function(id){return self.usersById[id]});
},
getChildComments: function(parentId) {
return this.getComments().filter(function(comment){return comment.parent == parentId});
},
getAttachments: function() {
return this.getComments().filter(function(comment){return comment.fileURL != undefined});
},
getOutermostParent: function(directParentId) {
var parentId = directParentId;
do {
var parentComment = this.commentsById[parentId];
parentId = parentComment.parent;
} while(parentComment.parent != null);
return parentComment;
},
createCommentJSON: function(textarea) {
var time = new Date().toISOString();
var commentJSON = {
id: 'c' + (this.getComments().length + 1), // Temporary id
parent: textarea.attr('data-parent') || null,
created: time,
modified: time,
content: this.getTextareaContent(textarea),
pings: this.getPings(textarea),
fullname: this.options.textFormatter(this.options.youText),
profilePictureURL: this.options.profilePictureURL,
createdByCurrentUser: true,
upvoteCount: 0,
userHasUpvoted: false
};
return commentJSON;
},
isAllowedToDelete: function(commentId) {
if(this.options.enableDeleting) {
var isAllowedToDelete = true;
if(!this.options.enableDeletingCommentWithReplies) {
$(this.getComments()).each(function(index, comment) {
if(comment.parent == commentId) isAllowedToDelete = false;
});
}
return isAllowedToDelete;
}
return false;
},
setToggleAllButtonText: function(toggleAllButton, toggle) {
var self = this;
var textContainer = toggleAllButton.find('span.text');
var caret = toggleAllButton.find('.caret');
var showExpandingText = function() {
var text = self.options.textFormatter(self.options.viewAllRepliesText);
var replyCount = toggleAllButton.siblings('.comment').length;
text = text.replace('__replyCount__', replyCount);
textContainer.text(text);
};
var hideRepliesText = this.options.textFormatter(this.options.hideRepliesText);
if(toggle) {
// Toggle text
if(textContainer.text() == hideRepliesText) {
showExpandingText();
} else {
textContainer.text(hideRepliesText);
}
// Toggle direction of the caret
caret.toggleClass('up');
} else {
// Update text if necessary
if(textContainer.text() != hideRepliesText) {
showExpandingText();
}
}
},
adjustTextareaHeight: function(textarea, focus) {
var textareaBaseHeight = 2.2;
var lineHeight = 1.45;
var setRows = function(rows) {
var height = textareaBaseHeight + (rows - 1) * lineHeight;
textarea.css('height', height + 'em');
};
textarea = $(textarea);
var rowCount = focus == true ? this.options.textareaRowsOnFocus : this.options.textareaRows;
do {
setRows(rowCount);
rowCount++;
var isAreaScrollable = textarea[0].scrollHeight > textarea.outerHeight();
var maxRowsUsed = this.options.textareaMaxRows == false ?
false : rowCount > this.options.textareaMaxRows;
} while(isAreaScrollable && !maxRowsUsed);
},
clearTextarea: function(textarea) {
textarea.empty().trigger('input');
},
getTextareaContent: function(textarea, humanReadable) {
var textareaClone = textarea.clone();
// Remove reply-to tag
textareaClone.find('.reply-to.tag').remove();
// Replace tags with text values
textareaClone.find('.tag.hashtag').replaceWith(function(){
return humanReadable ? $(this).val() : '#' + $(this).attr('data-value');
});
textareaClone.find('.tag.ping').replaceWith(function(){
return humanReadable ? $(this).val() : '@' + $(this).attr('data-value');
});
var ce = $('<pre/>').html(textareaClone.html());
ce.find('div, p, br').replaceWith(function() { return '\n' + this.innerHTML; });
// Trim leading spaces
var text = ce.text().replace(/^\s+/g, '');
// Normalize spaces
var text = this.normalizeSpaces(text);
return text;
},
getFormattedCommentContent: function(commentModel, replaceNewLines) {
var html = this.escape(commentModel.content);
html = this.linkify(html);
html = this.highlightTags(commentModel, html);
if(replaceNewLines) html = html.replace(/(?:\n)/g, '<br>');
return html;
},
getPings: function(textarea) {
return $.map(textarea.find('.ping'), function(el){return parseInt($(el).attr('data-value'))});
},
moveCursorToEnd: function(el) {
el = $(el)[0];
// Trigger input to adjust size
$(el).trigger('input');
// Scroll to bottom
$(el).scrollTop(el.scrollHeight);
// Move cursor to end
if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') {
var range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (typeof document.body.createTextRange != 'undefined') {
var textRange = document.body.createTextRange();
textRange.moveToElementText(el);
textRange.collapse(false);
textRange.select();
}
// Focus
el.focus();
},
escape: function(inputText) {
return $('<pre/>').text(this.normalizeSpaces(inputText)).html();
},
normalizeSpaces: function(inputText) {
return inputText.replace(new RegExp('\u00a0', 'g'), ' '); // Convert non-breaking spaces to reguar spaces
},
after: function(times, func) {
var self = this;
return function() {
times--;
if (times == 0) {
return func.apply(self, arguments);
}
}
},
highlightTags: function(commentModel, html) {
if(this.options.enableHashtags) html = this.highlightHashtags(commentModel, html);
if(this.options.enablePinging) html = this.highlightPings(commentModel, html);
return html;
},
highlightHashtags: function(commentModel, html) {
var self = this;
if(html.indexOf('#') != -1) {
var __createTag = function(tag) {
var tag = self.createTagElement('#' + tag, 'hashtag', tag);
return tag[0].outerHTML;
}
var regex = /(^|\s)#([a-zäöüß\d-_]+)/gim;
html = html.replace(regex, function($0, $1, $2){
return $1 + __createTag($2);
});
}
return html;
},
highlightPings: function(commentModel, html) {
var self = this;
if(html.indexOf('@') != -1) {
var __createTag = function(user) {
var tag = self.createTagElement('@' + user.fullname, 'ping', user.id);
return tag[0].outerHTML;
}
var highlightedHtml = '';
$(commentModel.pings).each(function(index, id) {
if(id in self.usersById) {
var user = self.usersById[id];
var pingText = '@' + user.fullname;
var endIndex = html.indexOf(pingText) + pingText.length;
var current = html.slice(0, endIndex);
highlightedHtml += current.replace(pingText, __createTag(user));
html = html.slice(endIndex);
}
});
highlightedHtml += html;
return highlightedHtml;
}
return html;
},
linkify: function(inputText) {
var replacedText, replacePattern1, replacePattern2, replacePattern3;
// URLs starting with http://, https://, file:// or ftp://
replacePattern1 = /(^|\s)((https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, '$1<a href="$2" target="_blank">$2</a>');
// URLs starting with "www." (without // before it, or it'd re-link the ones done above).
replacePattern2 = /(^|\s)(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
// Change email addresses to mailto:: links.
replacePattern3 = /(^|\s)(([a-zA-Z0-9\-\_\.]+)@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '$1<a href="mailto:$2">$2</a>');
// If there are hrefs in the original text, let's split
// the text up and only work on the parts that don't have urls yet.
var count = inputText.match(/<a href/g) || [];
if(count.length > 0){
// Keep delimiter when splitting
var splitInput = inputText.split(/(<\/a>)/g);
for (var i = 0 ; i < splitInput.length ; i++){
if(splitInput[i].match(/<a href/g) == null){
splitInput[i] = splitInput[i].replace(replacePattern1, '<a href="$1" target="_blank">$1</a>').replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>').replace(replacePattern3, '<a href="mailto:$1">$1</a>');
}
}
var combinedReplacedText = splitInput.join('');
return combinedReplacedText;
} else {
return replacedText;
}
},
waitUntil: function(condition, callback) {
var self = this;
if(condition()) {
callback();
} else {
setTimeout(function() {
self.waitUntil(condition, callback);
}, 100);
}
},
applyInternalMappings: function(commentJSON) {
// Inverting field mappings
var invertedMappings = {};
var mappings = this.options.fieldMappings;
for (var prop in mappings) {
if(mappings.hasOwnProperty(prop)) {
invertedMappings[mappings[prop]] = prop;
}
}
return this.applyMappings(invertedMappings, commentJSON);
},
applyExternalMappings: function(commentJSON) {
var mappings = this.options.fieldMappings;
return this.applyMappings(mappings, commentJSON);
},
applyMappings: function(mappings, commentJSON) {
var result = {};
for(var key1 in commentJSON) {
if(key1 in mappings) {
var key2 = mappings[key1];
result[key2] = commentJSON[key1];
}
}
return result;
}
};
$.fn.comments = function(options) {
return this.each(function() {
var comments = Object.create(Comments);
$.data(this, 'comments', comments);
comments.init(options || {}, this);
});
};
}));