jquery-comments
jquery-comments is a jQuery plugin for implementing an out-of-the-box commenting solution to any web application with an existing backend. It provides all the UI functionalities and ties them to callbacks that let you easily define what you want to do with the data. The library is highly customizable and very easy to integrate thanks to a wide variety of settings.
Features
- Commenting
- Replying (nested comments)
- Editing comments
- Deleting comments
- Upvoting comments
- Uploading attachments
- Hashtags
- Pinging users
- Enabling/disabling functionalities
- Localization
- Time formatting
- Field mappings
- Callbacks
- Fully responsive and mobile compatible
- Miscellaneous settings
Demo
http://viima.github.io/jquery-comments/demo/
Quick start
1) Add the following to your HTML file
<link rel="stylesheet" type="text/css" href="css/jquery-comments.css">
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery-comments.js"></script>
2) Initialize the library
$('#comments-container').comments({
profilePictureURL: 'https://viima-app.s3.amazonaws.com/media/public/defaults/user-icon.png',
getComments: function(success, error) {
var commentsArray = [{
id: 1,
created: '2015-10-01',
content: 'Lorem ipsum dolort sit amet',
fullname: 'Simon Powell',
upvote_count: 2,
user_has_upvoted: false
}];
success(commentsArray);
}
});
If you are not using Font Awesome for icons, you should replace the icons with custom images by overriding following options when initializing the library:
spinnerIconURL: '',
noCommentsIconURL: '',
closeIconURL: '',
upvoteIconURL: '', // Only if upvoting is enabled
replyIconURL: '', // Only if replying is enabled
uploadIconURL: '', // Only if attachments are enabled
attachmentIconURL: '', // Only if attachments are enabled
Example data
The library expects the comment (and attachment) data to be provided with the structure demonstrated below. Please note that you can use fieldMappings
to map the field names to match with your API.
{
"id": 4,
"parent": 3,
"created": "2015-01-04",
"modified": "2015-01-04",
"content": "Check out this video",
"attachments": [
{
"id": 1,
"file": "http://www.w3schools.com/html/mov_bbb.mp4",
"mime_type": "video/mp4",
},
],
"creator": 4,
"fullname": "Todd Brown",
"profile_picture_url": "https://viima-app.s3.amazonaws.com/media/public/defaults/user-icon.png",
"created_by_admin": false,
"created_by_current_user": false,
"upvote_count": 0,
"user_has_upvoted": false,
"is_new": true
}
Configuration options
Current user | |
---|---|
profilePictureURL |
An ""
$('#comments-container').comments({ profilePictureURL: '/user_profiles/user-icon.png' }); |
currentUserIsAdmin |
A false
$('#comments-container').comments({ currentUserIsAdmin: true }); |
Images | |
spinnerIconURL |
An ""
$('#comments-container').comments({ spinnerIconURL: '/img/spinner.gif' }); |
upvoteIconURL |
An ""
$('#comments-container').comments({ upvoteIconURL: '/img/upvote-icon.png' }); |
replyIconURL |
An ""
$('#comments-container').comments({ replyIconURL: '/img/reply-icon.png' }); |
uploadIconURL |
An ""
$('#comments-container').comments({ uploadIconURL: '/img/upload-icon.png' }); |
attachmentIconURL |
An ""
$('#comments-container').comments({ attachmentIconURL: '/img/attachment-icon.png' }); |
closeIconURL |
An ""
$('#comments-container').comments({ closeIconURL: '/img/close-icon.png' }); |
noCommentsIconURL |
An ""
$('#comments-container').comments({ noCommentsIconURL: '/img/no-comments-icon.png' }); |
Strings | |
textareaPlaceholderText |
A "Add a comment"
$('#comments-container').comments({ textareaPlaceholderText: 'Leave a comment' }); |
newestText |
A "Newest"
$('#comments-container').comments({ newestText: 'New' }); |
oldestText |
A "Oldest"
$('#comments-container').comments({ oldestText: 'Old' }); |
popularText |
A "Popular"
$('#comments-container').comments({ popularText: 'Most popular' }); |
attachmentsText |
A "Attachments"
$('#comments-container').comments({ attachmentsText: 'Show attachments' }); |
sendText |
A "Send"
$('#comments-container').comments({ sendText: 'Comment' }); |
replyText |
A "Reply"
$('#comments-container').comments({ replyText: 'Answer' }); |
editText |
A "Edit"
$('#comments-container').comments({ editText: 'Modify' }); |
editedText |
A "Edited"
$('#comments-container').comments({ editedText: 'Modified' }); |
youText |
A "You"
$('#comments-container').comments({ youText: 'Me' }); |
saveText |
A "Save"
$('#comments-container').comments({ saveText: 'Update' }); |
deleteText |
A "Delete"
$('#comments-container').comments({ deleteText: 'Remove' }); |
viewAllRepliesText |
A "View all __replyCount__ replies"
$('#comments-container').comments({ viewAllRepliesText: 'Show all replies (__replyCount__)' }); |
hideRepliesText |
A "Hide replies"
$('#comments-container').comments({ hideRepliesText: 'Hide' }); |
noCommentsText |
A "No comments"
$('#comments-container').comments({ noCommentsText: 'There are no comments' }); |
noAttachmentsText |
A "No attachments"
$('#comments-container').comments({ noAttachmentsText: 'There are no attachments' }); |
attachmentDropText |
A "Drop files here"
$('#comments-container').comments({ attachmentDropText: 'Drop here' }); |
Colors | highlightColor |
A #337AB7
$('#comments-container').comments({ highlightColor: '#23A6F0' }); |
deleteButtonColor |
A #C9302C
$('#comments-container').comments({ deleteButtonColor: 'red' }); |
Functionalities | |
enableReplying |
A true
$('#comments-container').comments({ enableReplying: false }); |
enableEditing |
A true
$('#comments-container').comments({ enableEditing: false }); |
enableUpvoting |
A true
$('#comments-container').comments({ enableUpvoting: false }); |
enableDeleting |
A true
$('#comments-container').comments({ enableDeleting: false }); |
enableDeletingCommentWithReplies |
A true
$('#comments-container').comments({ enableDeletingCommentWithReplies: false }); |
enableAttachments |
A false
$('#comments-container').comments({ enableAttachments: true }); |
enableHashtags |
A false
$('#comments-container').comments({ enableHashtags: true }); |
enablePinging |
A
false
$('#comments-container').comments({ enablePinging: true }); |
enableNavigation |
A true
$('#comments-container').comments({ enableNavigation: false }); |
postCommentOnEnter |
A false
$('#comments-container').comments({ postCommentOnEnter: true }); |
forceResponsive |
A false
$('#comments-container').comments({ forceResponsive: true }); |
readOnly |
A false
$('#comments-container').comments({ readOnly: true }); |
Field mappings | |
fieldMappings |
A id // Required parent // Required if replying is enabled created // Required modified // Required if editing is enabled content // Optional attachments // Required if attachments are enabled pings // Required if pinging is enabled creator // Required if pinging is enabled fullname // Required profilePictureURL // Optional isNew // Optional createdByAdmin // Optional createdByCurrentUser // Required if editing is enabled upvoteCount // Required if upvoting is enabled userHasUpvoted // Required if upvoting is enabled fieldMappings: { id: 'id', parent: 'parent', created: 'created', modified: 'modified', content: 'content', attachments: 'attachments', pings: 'pings', creator: 'creator', fullname: 'fullname', profilePictureURL: 'profile_picture_url', isNew: 'is_new', createdByAdmin: 'created_by_admin', createdByCurrentUser: 'created_by_current_user', upvoteCount: 'upvote_count', userHasUpvoted: 'user_has_upvoted' } $('#comments-container').comments({ fieldMappings: { parent: 'comment_id', modified: 'edited', fullname: 'name', profilePictureURL: 'user_image', upvoteCount: 'upvotes', } }); |
Callbacks | |
refresh |
A callback function() {} $('#comments-container').comments({ refresh: function() { $('#comments-container').addClass('rendered'); } }); |
getComments |
A callback function(success, error) { success([]); } $('#comments-container').comments({ getComments: function(success, error) { $.ajax({ type: 'get', url: '/api/comments/', success: function(commentsArray) { success(commentsArray) }, error: error }); } }); |
searchUsers |
A callback function(term, success, error) { success([]); } $('#comments-container').comments({ searchUsers: function(term, success, error) { $.ajax({ type: 'get', url: '/api/users/?search=' + term, success: function(userArray) { success(userArray) }, error: error }); } }); |
postComment |
A callback function(commentJSON, success, error) { success(commentJSON); } $('#comments-container').comments({ postComment: function(commentJSON, success, error) { $.ajax({ type: 'post', url: '/api/comments/', data: commentJSON, success: function(comment) { success(comment) }, error: error }); } }); |
putComment |
A callback function(commentJSON, success, error) { success(commentJSON); } $('#comments-container').comments({ putComment: function(commentJSON, success, error) { $.ajax({ type: 'put', url: '/api/comments/' + commentJSON.id, data: commentJSON, success: function(comment) { success(comment) }, error: error }); } }); |
deleteComment |
A callback function(commentJSON, success, error) { success(); } $('#comments-container').comments({ deleteComment: function(commentJSON, success, error) { $.ajax({ type: 'delete', url: '/api/comments/' + commentJSON.id, success: success, error: error }); } }); |
upvoteComment |
A callback function(commentJSON, success, error) { success(commentJSON); } $('#comments-container').comments({ upvoteComment: function(commentJSON, success, error) { var commentURL = '/api/comments/' + commentJSON.id; var upvotesURL = commentURL + '/upvotes/'; if(commentJSON.userHasUpvoted) { $.ajax({ type: 'post', url: upvotesURL, data: { comment: commentJSON.id }, success: function() { success(commentJSON) }, error: error }); } else { $.ajax({ type: 'delete', url: upvotesURL + upvoteId, success: function() { success(commentJSON) }, error: error }); } } }); |
validateAttachments |
A callback function(attachments, callback) { callback(attachments); } $('#comments-container').comments({ validateAttachments: function(attachments, callback) { var validAttachments = []; $(attachments).each(function(index, attachment) { var maxFileSizeMb = 10; var fileSizeMb = attachment.file.size / 1000000; if(fileSizeMb <= maxFileSizeMb) { validAttachments.push(attachment); } }); callback(validAttachments); } }); |
hashtagClicked |
A callback function(hashtag) {} $('#comments-container').comments({ hashtagClicked: function(hashtag) { location.hash = 'tags/' + hashtag } }); |
pingClicked |
A callback function(ping) {} $('#comments-container').comments({ pingClicked: function(ping) { location.hash = 'users/' + ping } }); |
Formatters | |
textFormatter |
A callback function(text) { return text; } $('#comments-container').comments({ textFormatter: function(text) { return i18n.translate(text); } }); |
timeFormatter |
A callback function(time) { return new Date(time).toLocaleDateString(); } $('#comments-container').comments({ timeFormatter: function(time) { return moment(time).fromNow(); } }); |
Miscellaneous | |
defaultNavigationSortKey |
A "newest"
$('#comments-container').comments({ defaultNavigationSortKey: 'popularity' }); |
roundProfilePictures |
A false
$('#comments-container').comments({ roundProfilePictures: true }); |
textareaRows |
An 2
$('#comments-container').comments({ textareaRows: 1 }); |
textareaRowsOnFocus |
An 2
$('#comments-container').comments({ textareaRowsOnFocus: 4 }); |
textareaMaxRows |
An 5
$('#comments-container').comments({ textareaMaxRows: false }); |
maxRepliesVisible |
An 2
$('#comments-container').comments({ maxRepliesVisible: 3 }); |
Working with attachments
Saving attachments is a bit different than saving other fields of the comment as attachments cannot be presented in plain text unlike other fields; the attachments are sent to the API in binary format and the API returns the URL of the saved file instead of the original binary data due to performance reasons.
The data should be provided to the API using FormData interface. Also please make sure that your API supports content type multipart/form-data
to accept the binary files provided with the FormData. Unfortunately it's not possbile to provide hierarchical data structure with the FormData interface and thus the attachments must be sent to the API using a temporary field. The API must then get the files from that temporary field, save them to a file system and provide the URLs of those files in the attachments
field.
In the example below, the attachments are sent to the API in a field named attachments_to_be_created
. The API is expected to process the files from that field and provide the URL of the saved attachments in the attachments
field of the response. Please see Example data for expected output.
$('#comments-container').comments({ postComment: function(commentJSON, success, error) { // Create form data and append all other fields but attachments var formData = new FormData(); $(Object.keys(commentJSON)).each(function(index, key) { if(key != 'attachments') { var value = commentJSON[key]; if(value) formData.append(key, value); } }); // Append attachments to be created to the form data var attachmentsToBeCreated = commentJSON.attachments.filter(function(attachment){ return !attachment.id }); $(attachmentsToBeCreated).each(function(index, attachment) { formData.append('attachments_to_be_created', attachment.file); }); // Save the comment together with the attachments $.ajax({ type: 'post', url: '/api/comments/', data: formData, contentType: 'multipart/form-data', success: function(comment) { success(comment); }, error: error }); } });
Dependencies
- jQuery >= 1.9.0
- Font Awesome (optional)
- jquery-textcomplete (optional)
Maintainers
- Joona Tykkyläinen, Viima Solutions Oy
Browser support
IE9+ and all modern browsers
Copyright and license
Code and documentation copyright 2020 Viima Solutions Oy. Code released under the MIT license.