ep_comments_page
Advanced tools
Comparing version 0.0.35 to 0.1.0
@@ -6,2 +6,3 @@ var _ = require('ep_etherpad-lite/static/js/underscore'); | ||
var readOnlyManager = require("ep_etherpad-lite/node/db/ReadOnlyManager.js"); | ||
var shared = require('./static/js/shared'); | ||
@@ -30,2 +31,28 @@ exports.getComments = function (padId, callback) | ||
exports.deleteComment = function (padId, commentId, callback) | ||
{ | ||
db.get('comments:' + padId, function(err, comments) | ||
{ | ||
if(ERR(err, callback)) return; | ||
// the entry doesn't exist so far, let's create it | ||
if(comments == null) comments = {}; | ||
delete comments[commentId]; | ||
db.set("comments:" + padId, comments); | ||
callback(padId, commentId); | ||
}); | ||
}; | ||
exports.deleteComments = function (padId, callback) | ||
{ | ||
db.remove('comments:' + padId, function(err) | ||
{ | ||
if(ERR(err, callback)) return; | ||
callback(null); | ||
}); | ||
}; | ||
exports.addComment = function(padId, data, callback) | ||
@@ -61,4 +88,4 @@ { | ||
var commentIds = _.map(data, function(commentData) { | ||
//create the new comment | ||
var commentId = "c-" + randomString(16); | ||
//if the comment was copied it already has a commentID, so we don't need create one | ||
var commentId = commentData.commentId || shared.generateCommentId(); | ||
@@ -73,3 +100,2 @@ var comment = { | ||
}; | ||
//add the entry for this pad | ||
@@ -89,2 +115,20 @@ comments[commentId] = comment; | ||
exports.copyComments = function(originalPadId, newPadID, callback) | ||
{ | ||
//get the comments of original pad | ||
db.get('comments:' + originalPadId, function(err, originalComments) { | ||
if(ERR(err, callback)) return; | ||
var copiedComments = _.mapObject(originalComments, function(thisComment, thisCommentId) { | ||
// make sure we have different copies of the comment between pads | ||
return _.clone(thisComment); | ||
}); | ||
//save the comments on new pad | ||
db.set('comments:' + newPadID, copiedComments); | ||
callback(null); | ||
}); | ||
}; | ||
exports.getCommentReplies = function (padId, callback){ | ||
@@ -109,2 +153,9 @@ // We need to change readOnly PadIds to Normal PadIds | ||
exports.deleteCommentReplies = function (padId, callback){ | ||
db.remove('comment-replies:' + padId, function(err) | ||
{ | ||
if(ERR(err, callback)) return; | ||
callback(null); | ||
}); | ||
}; | ||
@@ -168,2 +219,19 @@ exports.addCommentReply = function(padId, data, callback){ | ||
exports.copyCommentReplies = function(originalPadId, newPadID, callback){ | ||
//get the replies of original pad | ||
db.get('comment-replies:' + originalPadId, function(err, originalReplies){ | ||
if(ERR(err, callback)) return; | ||
var copiedReplies = _.mapObject(originalReplies, function(thisReply, thisReplyId) { | ||
// make sure we have different copies of the reply between pads | ||
return _.clone(thisReply); | ||
}); | ||
//save the comment replies on new pad | ||
db.set('comment-replies:' + newPadID, copiedReplies); | ||
callback(null); | ||
}); | ||
}; | ||
exports.changeAcceptedState = function(padId, commentId, state, callback){ | ||
@@ -210,1 +278,37 @@ // Given a comment we update that comment to say the change was accepted or reverted | ||
} | ||
exports.changeCommentText = function(padId, commentId, commentText, callback){ | ||
var commentTextIsNotEmpty = commentText.length > 0; | ||
if(commentTextIsNotEmpty){ | ||
// Given a comment we update the comment text | ||
// We need to change readOnly PadIds to Normal PadIds | ||
var isReadOnly = padId.indexOf("r.") === 0; | ||
if(isReadOnly){ | ||
readOnlyManager.getPadId(padId, function(err, rwPadId){ | ||
padId = rwPadId; | ||
}); | ||
}; | ||
// If we're dealing with comment replies we need to a different query | ||
var prefix = "comments:"; | ||
if(commentId.substring(0,7) === "c-reply"){ | ||
prefix = "comment-replies:"; | ||
} | ||
//get the entry | ||
db.get(prefix + padId, function(err, comments){ | ||
if(ERR(err, callback)) return; | ||
//update the comment text | ||
comments[commentId].text = commentText; | ||
//save the comment updated back | ||
db.set(prefix + padId, comments); | ||
callback(null); | ||
}); | ||
}else{// don't save comment text blank | ||
callback(true); | ||
} | ||
} |
@@ -6,8 +6,2 @@ | ||
function padExists(padID){ | ||
padManager.doesPadExists(padID, function(err, exists){ | ||
return exists; | ||
}); | ||
} | ||
exports.getPadComments = function(padID, callback) | ||
@@ -14,0 +8,0 @@ { |
@@ -16,2 +16,4 @@ { | ||
"hooks": { | ||
"padRemove": "ep_comments_page/index", | ||
"padCopy": "ep_comments_page/index", | ||
"socketio": "ep_comments_page/index", | ||
@@ -18,0 +20,0 @@ "expressCreateServer": "ep_comments_page/index", |
51
index.js
@@ -8,3 +8,15 @@ var eejs = require('ep_etherpad-lite/node/eejs/'); | ||
var apiUtils = require('./apiUtils'); | ||
var _ = require('ep_etherpad-lite/static/js/underscore'); | ||
exports.padRemove = function(hook_name, context, callback) { | ||
commentManager.deleteCommentReplies(context.padID, function() { | ||
commentManager.deleteComments(context.padID, callback); | ||
}); | ||
} | ||
exports.padCopy = function(hook_name, context, callback) { | ||
commentManager.copyComments(context.originalPad.id, context.destinationID, function() { | ||
commentManager.copyCommentReplies(context.originalPad.id, context.destinationID, callback); | ||
}); | ||
} | ||
exports.handleMessageSecurity = function(hook_name, context, callback){ | ||
@@ -64,2 +76,11 @@ if(context.message && context.message.data && context.message.data.apool){ | ||
socket.on('deleteComment', function(data, callback) { | ||
// delete the comment on the database | ||
commentManager.deleteComment(data.padId, data.commentId, function (){ | ||
// Broadcast to all other users that this comment was deleted | ||
socket.broadcast.to(data.padId).emit('commentDeleted', data.commentId); | ||
}); | ||
}); | ||
socket.on('revertChange', function(data, callback) { | ||
@@ -83,2 +104,32 @@ // Broadcast to all other users that this change was accepted. | ||
socket.on('bulkAddComment', function (padId, data, callback) { | ||
commentManager.bulkAddComments(padId, data, function(error, commentsId, comments){ | ||
socket.broadcast.to(padId).emit('pushAddCommentInBulk'); | ||
var commentWithCommentId = _.object(commentsId, comments); // {c-123:data, c-124:data} | ||
callback(commentWithCommentId) | ||
}); | ||
}); | ||
socket.on('bulkAddCommentReplies', function(padId, data, callback){ | ||
commentManager.bulkAddCommentReplies(padId, data, function (err, repliesId, replies){ | ||
socket.broadcast.to(padId).emit('pushAddCommentReply', repliesId, replies); | ||
var repliesWithReplyId = _.zip(repliesId, replies); | ||
callback(repliesWithReplyId); | ||
}); | ||
}); | ||
socket.on('updateCommentText', function(data, callback) { | ||
// Broadcast to all other users that the comment text was changed. | ||
// Note that commentId here can either be the commentId or replyId.. | ||
var padId = data.padId; | ||
var commentId = data.commentId; | ||
var commentText = data.commentText; | ||
commentManager.changeCommentText(padId, commentId, commentText, function(err) { | ||
if(!err){ | ||
socket.broadcast.to(padId).emit('textCommentUpdated', commentId, commentText); | ||
} | ||
callback(err); | ||
}); | ||
}); | ||
socket.on('addCommentReply', function (data, callback) { | ||
@@ -85,0 +136,0 @@ var padId = data.padId; |
@@ -5,15 +5,17 @@ { | ||
"ep_comments_page.add_comment.title" : "Kommentar zur Auswahl hinzufügen", | ||
"ep_comments_page.add_comment.hint" : "Bitte wählen Sie zuerst den zu kommentierenden Text aus", | ||
"ep_comments_page.delete_comment.title" : "Diesen Kommentar löschen", | ||
"ep_comments_page.show_comments" : "Kommentare anzeigen", | ||
"ep_comments_page.comments_template.suggested_change" : "Vorgeschlagene Änderung:", | ||
"ep_comments_page.comments_template.from" : "von:", | ||
"ep_comments_page.comments_template.suggested_change" : "Vorgeschlagene Änderung", | ||
"ep_comments_page.comments_template.from" : "von", | ||
"ep_comments_page.comments_template.accept_change.value" : "Änderung akzeptieren", | ||
"ep_comments_page.comments_template.revert_change.value" : "Änderung zurücknehmen", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Vorgeschlagene Änderung von:", | ||
"ep_comments_page.comments_template.suggest_change_from" : "von:", | ||
"ep_comments_page.comments_template.to" : "zu:", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Vorgeschlagene Änderung von", | ||
"ep_comments_page.comments_template.suggest_change_from" : "von", | ||
"ep_comments_page.comments_template.to" : "zu", | ||
"ep_comments_page.comments_template.include_suggestion" : "Änderung vorschlagen", | ||
"ep_comments_page.comments_template.comment.value" : "Kommentar", | ||
"ep_comments_page.comments_template.cancel.value" : "Abbrechen", | ||
"ep_comments_page.comments_template.reply_input_label":"antworten: (mit ENTER)", | ||
"ep_comments_page.comments_template.reply.value": "Antworten", | ||
"ep_comments_page.comments_template.reply.placeholder": "Antworten", | ||
"ep_comments_page.time.seconds.past" : "vor {{count}} Sekunden", | ||
@@ -20,0 +22,0 @@ "ep_comments_page.time.seconds.future" : "{{count}} Sekunden von jetzt an", |
@@ -5,15 +5,18 @@ { | ||
"ep_comments_page.add_comment.title" : "Add new comment on selection", | ||
"ep_comments_page.add_comment.hint" : "Please first select the text to comment", | ||
"ep_comments_page.delete_comment.title" : "Delete this comment", | ||
"ep_comments_page.edit_comment.title" : "Edit this comment", | ||
"ep_comments_page.show_comments" : "Show Comments", | ||
"ep_comments_page.comments_template.suggested_change" : "Suggested Change:", | ||
"ep_comments_page.comments_template.from" : "From:", | ||
"ep_comments_page.comments_template.suggested_change" : "Suggested Change", | ||
"ep_comments_page.comments_template.from" : "From", | ||
"ep_comments_page.comments_template.accept_change.value" : "Accept Change", | ||
"ep_comments_page.comments_template.revert_change.value" : "Revert Change", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Suggested change From:", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Suggest change From:", | ||
"ep_comments_page.comments_template.to" : "To:", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Suggested change From", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Suggest change From", | ||
"ep_comments_page.comments_template.to" : "To", | ||
"ep_comments_page.comments_template.include_suggestion" : "Include suggested change", | ||
"ep_comments_page.comments_template.comment.value" : "Comment", | ||
"ep_comments_page.comments_template.cancel.value" : "Cancel", | ||
"ep_comments_page.comments_template.reply_input_label":"Your Reply (hit ENTER to send)", | ||
"ep_comments_page.comments_template.reply.value" : "Reply", | ||
"ep_comments_page.comments_template.reply.placeholder" : "Reply", | ||
"ep_comments_page.time.seconds.past" : "{{count}} seconds ago", | ||
@@ -48,3 +51,5 @@ "ep_comments_page.time.seconds.future" : "{{count}} seconds from now", | ||
"ep_comments_page.time.centuries.past" : "{{count}} centuries ago", | ||
"ep_comments_page.time.centuries.future" : "{{count}} centuries from now" | ||
"ep_comments_page.time.centuries.future" : "{{count}} centuries from now", | ||
"ep_comments_page.comments_template.edit_comment.save" : "save", | ||
"ep_comments_page.comments_template.edit_comment.cancel" :"cancel" | ||
} |
@@ -5,15 +5,17 @@ { | ||
"ep_comments_page.add_comment.title" : "Annoter la sélection", | ||
"ep_comments_page.add_comment.hint" : "Vous devez d'abord sélectionner un texte à annoter", | ||
"ep_comments_page.delete_comment.title" : "Supprimer cette annotation", | ||
"ep_comments_page.show_comments" : "Afficher les annotations", | ||
"ep_comments_page.comments_template.suggested_change" : "Modification proposée :", | ||
"ep_comments_page.comments_template.from" : "Remplacer :", | ||
"ep_comments_page.comments_template.accept_change.value" : "Appliquer la modification", | ||
"ep_comments_page.comments_template.revert_change.value" : "Annuler la modification", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Modification proposée par :", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Proposer une modification de :", | ||
"ep_comments_page.comments_template.to" : "Par :", | ||
"ep_comments_page.comments_template.suggested_change" : "Modification proposée", | ||
"ep_comments_page.comments_template.from" : "Remplacer", | ||
"ep_comments_page.comments_template.accept_change.value" : "Appliquer la proposition", | ||
"ep_comments_page.comments_template.revert_change.value" : "Annuler la proposition", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Propose de remplacer", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Remplacer", | ||
"ep_comments_page.comments_template.to" : "Par", | ||
"ep_comments_page.comments_template.include_suggestion" : "Proposer une modification", | ||
"ep_comments_page.comments_template.comment.value" : "Annotation", | ||
"ep_comments_page.comments_template.cancel.value" : "Annuler", | ||
"ep_comments_page.comments_template.reply_input_label":"Votre réponse (pressez ENTRÉE pour valider)", | ||
"ep_comments_page.comments_template.reply.value":"Répondre", | ||
"ep_comments_page.comments_template.reply.placeholder":"Répondre", | ||
"ep_comments_page.time.seconds.past" : "il y a {{count}} secondes", | ||
@@ -20,0 +22,0 @@ "ep_comments_page.time.seconds.future" : "dans {{count}} secondes", |
@@ -5,15 +5,17 @@ { | ||
"ep_comments_page.add_comment.title" : "Dodaj nowy komentarz do sekcji", | ||
"ep_comments_page.add_comment.hint" : "Najpierw wybierz tekst do skomentowania", | ||
"ep_comments_page.delete_comment.title" : "Usuń komentarz", | ||
"ep_comments_page.show_comments" : "Pokaż komentarze", | ||
"ep_comments_page.comments_template.suggested_change" : "Sugerowane zmiany:", | ||
"ep_comments_page.comments_template.from" : "Od:", | ||
"ep_comments_page.comments_template.suggested_change" : "Sugerowane zmiany", | ||
"ep_comments_page.comments_template.from" : "Od", | ||
"ep_comments_page.comments_template.accept_change.value" : "Zaakceptuj zmiany", | ||
"ep_comments_page.comments_template.revert_change.value" : "Przywróc zmiany", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Sugerowana zmiana z:", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Zaproponuj zmiane z:", | ||
"ep_comments_page.comments_template.to" : "Do:", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Sugerowana zmiana z", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Zaproponuj zmiane z", | ||
"ep_comments_page.comments_template.to" : "Do", | ||
"ep_comments_page.comments_template.include_suggestion" : "Dołącz sugestie", | ||
"ep_comments_page.comments_template.comment.value" : "Komentarz", | ||
"ep_comments_page.comments_template.cancel.value" : "Anuluj", | ||
"ep_comments_page.comments_template.reply_input_label":"Odpowiedź (wciśnij ENTER aby wysłać)", | ||
"ep_comments_page.comments_template.reply.value": "Odpowiedź", | ||
"ep_comments_page.comments_template.reply.placeholder": "Odpowiedź", | ||
"ep_comments_page.time.seconds.past" : "{{count}} sekund temu", | ||
@@ -20,0 +22,0 @@ "ep_comments_page.time.seconds.future" : "{{count}} sekund od teraz", |
@@ -5,15 +5,16 @@ { | ||
"ep_comments_page.add_comment.title" : "Adicionar novo comentário ao texto selecionado", | ||
"ep_comments_page.add_comment.hint" : "Por favor, selecione primeiro o texto para comentar", | ||
"ep_comments_page.delete_comment.title" : "Apagar este comentário", | ||
"ep_comments_page.edit_comment.title" : "Editar este comentário", | ||
"ep_comments_page.show_comments" : "Mostrar Comentários", | ||
"ep_comments_page.comments_template.suggested_change" : "Alteração Sugerida:", | ||
"ep_comments_page.comments_template.from" : "De:", | ||
"ep_comments_page.comments_template.accept_change.value" : "Aceitar Sugestão", | ||
"ep_comments_page.comments_template.revert_change.value" : "Reverter Sugestão", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Alteração sugerida de:", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Sugerir alteração de:", | ||
"ep_comments_page.comments_template.to" : "Para:", | ||
"ep_comments_page.comments_template.suggested_change_from" : "Alteração sugerida de", | ||
"ep_comments_page.comments_template.suggest_change_from" : "Sugerir alteração de", | ||
"ep_comments_page.comments_template.to" : "Para", | ||
"ep_comments_page.comments_template.include_suggestion" : "Incluir alteração sugerida", | ||
"ep_comments_page.comments_template.comment.value" : "Comentário", | ||
"ep_comments_page.comments_template.cancel.value" : "Cancelar", | ||
"ep_comments_page.comments_template.reply_input_label":"Sua Resposta (clique ENTER para enviar)", | ||
"ep_comments_page.comments_template.reply.value":"Responder", | ||
"ep_comments_page.comments_template.reply.placeholder":"Responder", | ||
"ep_comments_page.time.seconds.past" : "{{count}} segundos atrás", | ||
@@ -48,3 +49,5 @@ "ep_comments_page.time.seconds.future" : "daqui a {{count}} segundos", | ||
"ep_comments_page.time.centuries.past" : "{{count}} séculos atrás", | ||
"ep_comments_page.time.centuries.future" : "daqui a {{count}} séculos" | ||
"ep_comments_page.time.centuries.future" : "daqui a {{count}} séculos", | ||
"ep_comments_page.comments_template.edit_comment.save" : "salvar", | ||
"ep_comments_page.comments_template.edit_comment.cancel" :"cancelar" | ||
} |
{ | ||
"description": "Adds comments on sidebar and link it to the text. Support for Page View, requires ep_page_view", | ||
"name": "ep_comments_page", | ||
"version": "0.0.35", | ||
"version": "0.1.0", | ||
"author": { | ||
@@ -27,2 +27,5 @@ "name": "Nicolas Lescop", | ||
}, | ||
"devDependencies": { | ||
"request" : "*" | ||
}, | ||
"engines": { | ||
@@ -29,0 +32,0 @@ "node": "*" |
@@ -23,3 +23,3 @@ // Easier access to outter pad | ||
var commentElm = getCommentsContainer().find('#'+ commentId); | ||
commentElm.removeClass('mouseover'); | ||
commentElm.removeClass('full-display'); | ||
@@ -29,28 +29,46 @@ // hide even the comment title | ||
getPadOuter().find('.comment-modal').hide(); | ||
var inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); | ||
inner.contents().find("head .comment-style").remove(); | ||
getPadOuter().find('.comment-modal').removeClass('popup-show'); | ||
}; | ||
var hideOpenedComments = function() { | ||
var openedComments = getCommentsContainer().find('.mouseover'); | ||
openedComments.removeClass('mouseover').hide(); | ||
getPadOuter().find('.comment-modal').hide(); | ||
} | ||
var hideAllComments = function() { | ||
getCommentsContainer().children().hide(); | ||
getCommentsContainer().find('.sidebar-comment').removeClass('full-display'); | ||
getPadOuter().find('.comment-modal').removeClass('popup-show'); | ||
} | ||
var highlightComment = function(commentId, e){ | ||
var highlightComment = function(commentId, e, editorComment){ | ||
var container = getCommentsContainer(); | ||
var commentElm = container.find('#'+ commentId); | ||
var commentsVisible = container.is(":visible"); | ||
if(commentsVisible) { | ||
// sidebar view highlight | ||
commentElm.addClass('mouseover'); | ||
var inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); | ||
if (container.is(":visible")) { | ||
// hide all other comments | ||
container.find('.sidebar-comment').each(function() { | ||
inner.contents().find("head .comment-style").remove(); | ||
$(this).removeClass('full-display') | ||
}); | ||
// Then highlight new comment | ||
commentElm.addClass('full-display'); | ||
// now if we apply a class such as mouseover to the editor it will go shitty | ||
// so what we need to do is add CSS for the specific ID to the document... | ||
// It's fucked up but that's how we do it.. | ||
var inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); | ||
inner.contents().find("head").append("<style class='comment-style'>."+commentId+"{ color: #a7680c !important }</style>"); | ||
} else { | ||
var commentElm = container.find('#'+ commentId); | ||
// make a full copy of the html, including listeners | ||
var commentElmCloned = commentElm.clone(true, true); | ||
// before of appending clear the css (like top positionning) | ||
commentElmCloned.attr('style', ''); | ||
// fix checkbox, because as we are duplicating the sidebar-comment, we lose unique input names | ||
commentElmCloned.find('.label-suggestion-checkbox').click(function() { | ||
$(this).siblings('input[type="checkbox"]').click(); | ||
}) | ||
// hovering comment view | ||
getPadOuter().find('.comment-modal-comment').html(commentElm.html()); | ||
getPadOuter().find('.comment-modal-comment').html('').append(commentElmCloned); | ||
var padInner = getPadOuter().find('iframe[name="ace_inner"]') | ||
// get modal position | ||
@@ -61,10 +79,19 @@ var containerWidth = getPadOuter().find('#outerdocbody').outerWidth(true); | ||
var targetTop = $(e.target).offset().top; | ||
if (editorComment) { | ||
targetTop += parseInt(padInner.css('padding-top').split('px')[0]) | ||
targetTop += parseInt(padOuter.find('#outerdocbody').css('padding-top').split('px')[0]) | ||
} else { | ||
// mean we are clicking from a comment Icon | ||
var targetLeft = $(e.target).offset().left - 20; | ||
} | ||
// if positioning modal on target left will make part of the modal to be | ||
// out of screen, we place it closer to the middle of the screen | ||
if (targetLeft + modalWitdh > containerWidth) { | ||
targetLeft = containerWidth - modalWitdh - 2; | ||
targetLeft = containerWidth - modalWitdh - 25; | ||
} | ||
getPadOuter().find('.comment-modal').show().css({ | ||
left: targetLeft +"px", | ||
top: targetTop + 25 +"px" | ||
var editorCommentHeight = editorComment ? editorComment.outerHeight(true) : 30; | ||
getPadOuter().find('.comment-modal').addClass('popup-show').css({ | ||
left: targetLeft + "px", | ||
top: targetTop + editorCommentHeight +"px" | ||
}); | ||
@@ -78,4 +105,3 @@ } | ||
var commentElement = getPadOuter().find('#'+commentId); | ||
var targetTop = baseTop - 5; | ||
commentElement.css("top", targetTop+"px"); | ||
commentElement.css("top", baseTop+"px"); | ||
@@ -88,3 +114,3 @@ return commentElement; | ||
var commentElement = getPadOuter().find('#'+commentId); | ||
var expectedTop = (baseTop - 5) + "px"; | ||
var expectedTop = baseTop + "px"; | ||
return commentElement.css("top") === expectedTop; | ||
@@ -104,3 +130,2 @@ } | ||
exports.hideComment = hideComment; | ||
exports.hideOpenedComments = hideOpenedComments; | ||
exports.hideAllComments = hideAllComments; | ||
@@ -107,0 +132,0 @@ exports.highlightComment = highlightComment; |
@@ -10,17 +10,2 @@ var $ = require('ep_etherpad-lite/static/js/rjquery').$; | ||
// Indicates if screen has enough space on right margin to display icons | ||
var screenHasSpaceToDisplayIcons; | ||
var screenHasSpaceForIcons = function() { | ||
if (screenHasSpaceToDisplayIcons === undefined) calculateIfScreenHasSpaceForIcons(); | ||
return screenHasSpaceToDisplayIcons; | ||
} | ||
var calculateIfScreenHasSpaceForIcons = function() { | ||
var firstElementOnPad = getPadInner().find("#innerdocbody > div").first(); | ||
var rightMargin = firstElementOnPad.css("margin-right"); | ||
screenHasSpaceToDisplayIcons = rightMargin !== "0px"; | ||
} | ||
// Easier access to outer pad | ||
@@ -63,8 +48,7 @@ var padOuter; | ||
var highlightTargetTextOf = function(commentId) { | ||
getPadInner().find("head").append("<style>."+commentId+"{ color:orange }</style>"); | ||
getPadInner().find("head").append("<style class='comment-style'>."+commentId+"{ color: #a7680c !important }</style>"); | ||
} | ||
var removeHighlightOfTargetTextOf = function(commentId) { | ||
getPadInner().find("head").append("<style>."+commentId+"{ color:black }</style>"); | ||
// TODO this could potentially break ep_font_color | ||
var removeHighlightTargetText = function(commentId) { | ||
getPadInner().find("head .comment-style").remove(); | ||
} | ||
@@ -78,2 +62,3 @@ | ||
getPadOuter().find('#commentIcons').on("mouseover", ".comment-icon", function(e){ | ||
removeHighlightTargetText(); | ||
var commentId = targetCommentIdOf(e); | ||
@@ -83,3 +68,3 @@ highlightTargetTextOf(commentId); | ||
var commentId = targetCommentIdOf(e); | ||
removeHighlightOfTargetTextOf(commentId); | ||
removeHighlightTargetText(); | ||
}).on("click", ".comment-icon.active", function(e){ | ||
@@ -93,3 +78,3 @@ toggleActiveCommentIcon($(this)); | ||
// one comment box opened at a time | ||
commentBoxes.hideOpenedComments(); | ||
commentBoxes.hideAllComments(); | ||
var allActiveIcons = getPadOuter().find('#commentIcons').find(".comment-icon.active"); | ||
@@ -101,18 +86,6 @@ toggleActiveCommentIcon(allActiveIcons); | ||
var commentId = targetCommentIdOf(e); | ||
commentBoxes.showComment(commentId, e); | ||
commentBoxes.highlightComment(commentId, e); | ||
}); | ||
} | ||
// Listen to Page View enabling/disabling, to adjust #commentIcons position | ||
var addListenersToPageView = function() { | ||
$("#options-pageview").on("click", function() { | ||
getPadOuter().find('#outerdocbody').toggleClass("pageViewDisabled"); | ||
}); | ||
// add class if Page View is disabled already | ||
if(!$('#options-pageview').is(':checked')) { | ||
getPadOuter().find('#outerdocbody').addClass("pageViewDisabled"); | ||
} | ||
} | ||
// Listen to clicks on the page to be able to close comment when clicking | ||
@@ -164,7 +137,5 @@ // outside of it | ||
getPadOuter().find("#sidediv").after('<div id="commentIcons"></div>'); | ||
adjustIconsForNewScreenSize(); | ||
getPadOuter().find("#comments").addClass('with-icons'); | ||
addListenersToCommentIcons(); | ||
addListenersToCloseOpenedComment(); | ||
addListenersToPageView(); | ||
} | ||
@@ -178,3 +149,3 @@ | ||
var inlineComment = getPadInner().find(".comment."+commentId); | ||
var top = inlineComment.get(0).offsetTop + 5; | ||
var top = inlineComment.get(0).offsetTop; | ||
var iconsAtLine = getOrCreateIconsContainerAt(top); | ||
@@ -189,3 +160,3 @@ var icon = $('#commentIconTemplate').tmpl(comment); | ||
// we're only doing something if icons will be displayed at all | ||
if (!displayIcons() || !screenHasSpaceForIcons()) return; | ||
if (!displayIcons()) return; | ||
@@ -201,6 +172,6 @@ getPadOuter().find('#commentIcons').children().children().each(function(){ | ||
// we're only doing something if icons will be displayed at all | ||
if (!displayIcons() || !screenHasSpaceForIcons()) return; | ||
if (!displayIcons()) return; | ||
var icon = getPadOuter().find('#icon-'+commentId); | ||
var targetTop = baseTop+5; | ||
var targetTop = baseTop; | ||
var iconsAtLine = getOrCreateIconsContainerAt(targetTop); | ||
@@ -220,3 +191,3 @@ | ||
// we're only doing something if icons will be displayed at all | ||
if (!displayIcons() || !screenHasSpaceForIcons()) return false; | ||
if (!displayIcons()) return false; | ||
@@ -245,3 +216,3 @@ var iconClicked = getPadOuter().find('#commentIcons').find(".comment-icon.active"); | ||
if (!displayIcons() || !screenHasSpaceForIcons()) { | ||
if (!displayIcons()) { | ||
// if icons are not being displayed, we always show comments | ||
@@ -257,17 +228,2 @@ shouldShowComment = true; | ||
var adjustIconsForNewScreenSize = function() { | ||
// we're only doing something if icons will be displayed at all | ||
if (!displayIcons()) return; | ||
// now that screen has a different size, we need to force calculation | ||
// of flag used by screenHasSpaceForIcons() before calling the function | ||
calculateIfScreenHasSpaceForIcons(); | ||
if (screenHasSpaceForIcons()) { | ||
getPadOuter().find('#commentIcons').show(); | ||
} else { | ||
getPadOuter().find('#commentIcons').hide(); | ||
} | ||
} | ||
// Indicates if event was on one of the elements that does not close comment (any of the comment icons) | ||
@@ -285,3 +241,2 @@ var shouldNotCloseComment = function(e) { | ||
exports.shouldShow = shouldShow; | ||
exports.adjustIconsForNewScreenSize = adjustIconsForNewScreenSize; | ||
exports.shouldNotCloseComment = shouldNotCloseComment; |
@@ -1,24 +0,116 @@ | ||
exports.addTextOnClipboard = function(e, ace, padInner){ | ||
var commentIdOnSelection; | ||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; | ||
var _ = require('ep_etherpad-lite/static/js/underscore'); | ||
var shared = require('./shared'); | ||
exports.addTextOnClipboard = function(e, ace, padInner, removeSelection, comments, replies){ | ||
var commentIdOnFirstPositionSelected; | ||
var hasCommentOnSelection; | ||
ace.callWithAce(function(ace) { | ||
commentIdOnSelection = ace.ace_getCommentIdOnSelection(); | ||
commentIdOnFirstPositionSelected = ace.ace_getCommentIdOnFirstPositionSelected(); | ||
hasCommentOnSelection = ace.ace_hasCommentOnSelection(); | ||
}); | ||
// we check if all the selection is in the same comment, if so, we override the copy behavior | ||
if (commentIdOnSelection) { | ||
if(hasCommentOnSelection){ | ||
var commentsData; | ||
var range = padInner.contents()[0].getSelection().getRangeAt(0); | ||
var hiddenDiv = createHiddenDiv(range); | ||
var html = getHtml(hiddenDiv); | ||
// when the range selection is fully inside a tag, 'html' will have no HTML tag, so we have to | ||
var rawHtml = createHiddenDiv(range); | ||
var html = rawHtml; | ||
var onlyTextIsSelected = selectionHasOnlyText(rawHtml); | ||
// when the range selection is fully inside a tag, 'rawHtml' will have no HTML tag, so we have to | ||
// build it. Ex: if we have '<span>ab<b>cdef</b>gh</span>" and user selects 'de', the value of | ||
//'html' will be 'de', not '<b>de</b>' | ||
if (selectionHasOnlyText(html, hiddenDiv)) { | ||
html = buildHtmlToCopy(html, range); | ||
e.originalEvent.clipboardData.setData('text/copyCommentId', commentIdOnSelection); | ||
//'rawHtml' will be 'de', not '<b>de</b>'. As it is not possible to have two comments in the same text | ||
// commentIdOnFirstPositionSelected is the commentId in this partial selection | ||
if (onlyTextIsSelected) { | ||
var textSelected = rawHtml[0].textContent; | ||
html = buildHtmlToCopyWhenSelectionHasOnlyText(textSelected, range, commentIdOnFirstPositionSelected); | ||
} | ||
var commentIds = getCommentIds(html); | ||
commentsData = buildCommentsData(html, comments); | ||
var htmlToCopy = replaceCommentIdsWithFakeIds(commentsData, html) | ||
commentsData = JSON.stringify(commentsData); | ||
var replyData = getReplyData(replies, commentIds); | ||
replyData = JSON.stringify(replyData); | ||
e.originalEvent.clipboardData.setData('text/objectReply', replyData); | ||
e.originalEvent.clipboardData.setData('text/objectComment', commentsData); | ||
// here we override the default copy behavior | ||
e.originalEvent.clipboardData.setData('text/html', html); | ||
e.originalEvent.clipboardData.setData('text/html', htmlToCopy); | ||
e.preventDefault(); | ||
// if it is a cut event we have to remove the selection | ||
if(removeSelection){ | ||
padInner.contents()[0].execCommand("delete"); | ||
} | ||
} | ||
}; | ||
var getReplyData = function(replies, commentIds){ | ||
var replyData = {}; | ||
_.each(commentIds, function(commentId){ | ||
replyData = _.extend(getRepliesFromCommentId(replies, commentId), replyData); | ||
}); | ||
return replyData; | ||
}; | ||
var getRepliesFromCommentId = function(replies, commentId){ | ||
var repliesFromCommentID = {}; | ||
_.each(replies, function(reply, replyId){ | ||
if(reply.commentId === commentId){ | ||
repliesFromCommentID[replyId] = reply; | ||
} | ||
}); | ||
return repliesFromCommentID; | ||
}; | ||
var buildCommentIdToFakeIdMap = function(commentsData){ | ||
var commentIdToFakeId = {}; | ||
_.each(commentsData, function(comment, fakeCommentId){ | ||
var commentId = comment.data.originalCommentId; | ||
commentIdToFakeId[commentId] = fakeCommentId; | ||
}); | ||
return commentIdToFakeId; | ||
}; | ||
var replaceCommentIdsWithFakeIds = function(commentsData, html){ | ||
var commentIdToFakeId = buildCommentIdToFakeIdMap(commentsData); | ||
_.each(commentIdToFakeId, function(fakeCommentId, commentId){ | ||
$(html).find("." + commentId).removeClass(commentId).addClass(fakeCommentId); | ||
}); | ||
var htmlWithFakeCommentIds = getHtml(html); | ||
return htmlWithFakeCommentIds; | ||
}; | ||
var buildCommentsData = function(html, comments){ | ||
var commentsData = {}; | ||
var originalCommentIds = getCommentIds(html); | ||
_.each(originalCommentIds, function(originalCommentId){ | ||
var fakeCommentId = generateFakeCommentId(); | ||
var comment = comments[originalCommentId]; | ||
comment.data.originalCommentId = originalCommentId; | ||
commentsData[fakeCommentId] = comment; | ||
}); | ||
return commentsData; | ||
}; | ||
var generateFakeCommentId = function(){ | ||
var commentId = "fakecomment-" + randomString(16); | ||
return commentId; | ||
}; | ||
var getCommentIds = function(html){ | ||
var allSpans = $(html).find("span"); | ||
var commentIds = []; | ||
_.each(allSpans, function(span){ | ||
var cls = $(span).attr('class'); | ||
var classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); | ||
var commentId = (classCommentId) ? classCommentId[1] : false; | ||
if(commentId){ | ||
commentIds.push(commentId); | ||
} | ||
}); | ||
var uniqueCommentIds = _.uniq(commentIds); | ||
return uniqueCommentIds; | ||
}; | ||
var createHiddenDiv = function(range){ | ||
@@ -35,11 +127,21 @@ var content = range.cloneContents(); | ||
var selectionHasOnlyText = function(html, hiddenDiv){ | ||
var selectionHasOnlyText = function(rawHtml){ | ||
var html = getHtml(rawHtml); | ||
var htmlDecoded = htmlDecode(html); | ||
var text = $(hiddenDiv).text(); | ||
var text = $(rawHtml).text(); | ||
return htmlDecoded === text; | ||
}; | ||
var buildHtmlToCopy = function(html, range) { | ||
var buildHtmlToCopyWhenSelectionHasOnlyText = function(text, range, commentId) { | ||
var htmlWithSpans = buildHtmlWithTwoSpanTags(text, commentId); | ||
var html = buildHtmlWithFormattingTagsOfSelection(htmlWithSpans, range); | ||
var htmlToCopy = $.parseHTML("<div>" + html + "</div>"); | ||
return htmlToCopy; | ||
}; | ||
var buildHtmlWithFormattingTagsOfSelection = function(html, range) { | ||
var htmlOfParentNode = range.commonAncestorContainer.parentNode; | ||
var tags = getTagsInSelection(htmlOfParentNode); | ||
// this case happens when we got a selection with one or more styling (bold, italic, underline, strikethrough) | ||
@@ -50,7 +152,19 @@ // applied in all selection in the same range. For example, <b><i><u>text</u></i></b> | ||
} | ||
var htmlToCopy = "<span class='comment'>" + html + "</span>"; | ||
return htmlToCopy; | ||
}; | ||
return html; | ||
} | ||
// FIXME - Allow to copy a comment when user copies only one char | ||
// This is a hack to preserve the comment classes when user pastes a comment. When user pastes a span like this | ||
// <span class='comment c-124'>thing</span>, chrome removes the classes and keeps only the style of the class. With comments | ||
// chrome keeps the background-color. To avoid this we create two spans. The first one, <span class='comment c-124'>thi</span> | ||
// has the text until the last but one character and second one with the last character <span class='comment c-124'>g</span>. | ||
// Etherpad does a good job joining the two spans into one after the paste is triggered. | ||
var buildHtmlWithTwoSpanTags = function(text, commentId) { | ||
var firstSpan = '<span class="comment ' + commentId + '">'+ text.slice(0, -1) + '</span>'; // text until before last char | ||
var secondSpan = '<span class="comment ' + commentId + '">'+ text.slice(-1) + '</span>'; // last char | ||
return firstSpan + secondSpan; | ||
} | ||
var buildOpenTags = function(tags){ | ||
@@ -76,3 +190,3 @@ var openTags = ""; | ||
var tag; | ||
while($(htmlObject)[0].localName != "span"){ | ||
while($(htmlObject)[0].localName !== "span"){ | ||
var html = $(htmlObject).prop('outerHTML'); | ||
@@ -85,39 +199,55 @@ var stylingTagRegex = /<(b|i|u|s)>/.exec(html); | ||
return tags; | ||
} | ||
exports.addCommentClasses = function(e){ | ||
var commentId = e.originalEvent.clipboardData.getData('text/copyCommentId'); | ||
var target = e.target; | ||
if (commentId) { | ||
// we need to wait the paste process finishes completely, otherwise we will not have the target to add the necessary classes | ||
setTimeout(function() { | ||
addCommentClassesOnline(target, commentId); | ||
}, 0); | ||
} | ||
}; | ||
var addCommentClassesOnline = function (target, commentId) { | ||
var pastingOnEmptyLine = isEmptyLine(target); | ||
var targetElement; | ||
if (pastingOnEmptyLine){ | ||
targetElement = $(target).parent(); | ||
}else{ | ||
targetElement = getTargetOnLineWithContent(); | ||
exports.saveCommentsAndReplies = function(e){ | ||
var comments = e.originalEvent.clipboardData.getData('text/objectComment'); | ||
var replies = e.originalEvent.clipboardData.getData('text/objectReply'); | ||
if(comments && replies) { | ||
comments = JSON.parse(comments); | ||
replies = JSON.parse(replies); | ||
saveComments(comments); | ||
saveReplies(replies); | ||
} | ||
targetElement.addClass(commentId).addClass('comment'); | ||
}; | ||
var saveComments = function(comments){ | ||
var commentsToSave = {}; | ||
var padId = clientVars.padId; | ||
var getTargetOnLineWithContent = function() { | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]').contents(); | ||
var target = padInner.find("span[style='background-color: rgb(255, 250, 205);']"); | ||
return target; | ||
var mapOriginalCommentsId = pad.plugins.ep_comments_page.mapOriginalCommentsId; | ||
var mapFakeComments = pad.plugins.ep_comments_page.mapFakeComments; | ||
_.each(comments, function(comment, fakeCommentId){ | ||
var commentData = buildCommentData(comment, fakeCommentId); | ||
var newCommentId = shared.generateCommentId(); | ||
mapFakeComments[fakeCommentId] = newCommentId; | ||
var originalCommentId = comment.data.originalCommentId; | ||
mapOriginalCommentsId[originalCommentId] = newCommentId; | ||
commentsToSave[newCommentId] = comment; | ||
}); | ||
pad.plugins.ep_comments_page.saveCommentWithoutSelection(padId, commentsToSave); | ||
}; | ||
// an empty line has only a <br> | ||
var isEmptyLine = function(target) { | ||
return $(target).is("br"); | ||
var saveReplies = function(replies){ | ||
var repliesToSave = {}; | ||
var padId = clientVars.padId; | ||
var mapOriginalCommentsId = pad.plugins.ep_comments_page.mapOriginalCommentsId; | ||
_.each(replies, function(reply, replyId){ | ||
var originalCommentId = reply.commentId; | ||
// as the comment copied has got a new commentId, we set this id in the reply as well | ||
reply.commentId = mapOriginalCommentsId[originalCommentId]; | ||
repliesToSave[replyId] = reply; | ||
}); | ||
pad.plugins.ep_comments_page.saveCommentReplies(padId, repliesToSave); | ||
}; | ||
var buildCommentData = function(comment, fakeCommentId){ | ||
var commentData = {}; | ||
commentData.padId = clientVars.padId; | ||
commentData.comment = comment.data; | ||
commentData.comment.commentId = fakeCommentId; | ||
return commentData; | ||
}; | ||
// copied from https://css-tricks.com/snippets/javascript/unescape-html-in-js/ | ||
@@ -130,10 +260,84 @@ var htmlDecode = function(input) { | ||
exports.getCommentIdOnSelection = function() { | ||
// here we find the comment id on a position [line, column]. This function is used to get the comment id | ||
// of one line when there is ONLY text selected. E.g In the line with comment, <span class='comment...'>something</span>, | ||
// and user copies the text 'omethin'. The span tags are not copied only the text. So as the comment is | ||
// applied on the selection we get the commentId using the first position selected of the line. | ||
// P.S: It's not possible to have two or more comments when there is only text selected, because for each comment | ||
// created it's generated a <span> and to copy only the text it MUST NOT HAVE any tag on the selection | ||
exports.getCommentIdOnFirstPositionSelected = function() { | ||
var attributeManager = this.documentAttributeManager; | ||
var rep = this.rep; | ||
var selStartAttrib = _.object(attributeManager.getAttributesOnPosition(rep.selStart[0], rep.selStart[1])).comment; | ||
var selEndAttrib = _.object(attributeManager.getAttributesOnPosition(rep.selEnd[0], rep.selEnd[1] - 1)).comment; | ||
return selStartAttrib === selEndAttrib ? selStartAttrib : null; | ||
var commentId = _.object(attributeManager.getAttributesOnPosition(rep.selStart[0], rep.selStart[1])).comment; | ||
return commentId; | ||
}; | ||
exports.hasCommentOnSelection = function() { | ||
var hasComment; | ||
var attributeManager = this.documentAttributeManager; | ||
var rep = this.rep; | ||
var firstLineOfSelection = rep.selStart[0]; | ||
var firstColumn = rep.selStart[1]; | ||
var lastColumn = rep.selEnd[1]; | ||
var lastLineOfSelection = rep.selEnd[0]; | ||
var selectionOfMultipleLine = hasMultipleLineSelected(firstLineOfSelection, lastLineOfSelection); | ||
if(selectionOfMultipleLine){ | ||
hasComment = hasCommentOnMultipleLineSelection(firstLineOfSelection,lastLineOfSelection, rep, attributeManager); | ||
}else{ | ||
hasComment = hasCommentOnLine(firstLineOfSelection, firstColumn, lastColumn, attributeManager) | ||
} | ||
return hasComment; | ||
}; | ||
var hasCommentOnMultipleLineSelection = function(firstLineOfSelection, lastLineOfSelection, rep, attributeManager){ | ||
var foundLineWithComment = false; | ||
for (var line = firstLineOfSelection; line <= lastLineOfSelection && !foundLineWithComment; line++) { | ||
var firstColumn = getFirstColumnOfSelection(line, rep, firstLineOfSelection); | ||
var lastColumn = getLastColumnOfSelection(line, rep, lastLineOfSelection); | ||
var hasComment = hasCommentOnLine(line, firstColumn, lastColumn, attributeManager); | ||
if (hasComment){ | ||
foundLineWithComment = true; | ||
} | ||
} | ||
return foundLineWithComment; | ||
} | ||
var getFirstColumnOfSelection = function(line, rep, firstLineOfSelection){ | ||
return line !== firstLineOfSelection ? 0 : rep.selStart[1]; | ||
}; | ||
var getLastColumnOfSelection = function(line, rep, lastLineOfSelection){ | ||
var lastColumnOfSelection; | ||
if (line !== lastLineOfSelection) { | ||
lastColumnOfSelection = getLength(line, rep); // length of line | ||
}else{ | ||
lastColumnOfSelection = rep.selEnd[1] - 1; //position of last character selected | ||
} | ||
return lastColumnOfSelection; | ||
}; | ||
var hasCommentOnLine = function(lineNumber, firstColumn, lastColumn, attributeManager){ | ||
var foundCommentOnLine = false; | ||
for (var column = firstColumn; column <= lastColumn && !foundCommentOnLine; column++) { | ||
var commentId = _.object(attributeManager.getAttributesOnPosition(lineNumber, column)).comment; | ||
if (commentId !== undefined){ | ||
foundCommentOnLine = true; | ||
} | ||
} | ||
return foundCommentOnLine; | ||
}; | ||
var hasMultipleLineSelected = function(firstLineOfSelection, lastLineOfSelection){ | ||
return firstLineOfSelection !== lastLineOfSelection; | ||
}; | ||
var getLength = function(line, rep) { | ||
var nextLine = line + 1; | ||
var startLineOffset = rep.lines.offsetOfIndex(line); | ||
var endLineOffset = rep.lines.offsetOfIndex(nextLine); | ||
//lineLength without \n | ||
var lineLength = endLineOffset - startLineOffset - 1; | ||
return lineLength; | ||
}; |
@@ -9,2 +9,3 @@ /* TODO: | ||
var shared = require('./shared'); | ||
var $ = require('ep_etherpad-lite/static/js/rjquery').$; | ||
@@ -20,8 +21,10 @@ var _ = require('ep_etherpad-lite/static/js/underscore'); | ||
var events = require('ep_comments_page/static/js/copyPasteEvents'); | ||
var getCommentIdOnSelection = events.getCommentIdOnSelection; | ||
var getCommentIdOnFirstPositionSelected = events.getCommentIdOnFirstPositionSelected; | ||
var hasCommentOnSelection = events.hasCommentOnSelection; | ||
var browser = require('ep_etherpad-lite/static/js/browser'); | ||
var cssFiles = ['ep_comments_page/static/css/comment.css', 'ep_comments_page/static/css/commentIcon.css']; | ||
var UPDATE_COMMENT_LINE_POSITION_EVENT = 'updateCommentLinePosition'; | ||
/************************************************************************/ | ||
@@ -48,11 +51,7 @@ /* ep_comments Plugin */ | ||
this.commentReplies = {}; | ||
this.mapFakeComments = []; | ||
this.mapOriginalCommentsId = []; | ||
this.shouldCollectComment = false; | ||
this.init(); | ||
this.preCommentMarker = preCommentMark.init(this.ace); | ||
// If we're on a read only pad then hide the ability to attempt to merge a suggestion | ||
if(clientVars.readonly){ | ||
this.padInner.append( | ||
"<style>.comment-changeTo-approve," + | ||
".comment-reply-changeTo-approve{display:none;}</style>"); | ||
} | ||
} | ||
@@ -85,4 +84,5 @@ | ||
self.collectCommentReplies(); | ||
self.commentRepliesListen(); | ||
} | ||
self.commentRepliesListen(); | ||
self.commentListen(); | ||
}); | ||
@@ -94,30 +94,3 @@ | ||
// console.log('pushComment', comment); | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
var count_comments=0; | ||
for(var key in self.comments) {count_comments++;} | ||
var padComment = this.padInner.contents().find('.comment'); | ||
if( count_comments > padComment.length ) { | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
var count_comments=0; | ||
for(var key in self.comments) {count_comments++;} | ||
var padComment = this.padInner.contents().find('.comment'); | ||
if( count_comments > padComment.length ) { | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
var count_comments=0; | ||
for(var key in self.comments) {count_comments++;} | ||
var padComment = this.padInner.contents().find('.comment'); | ||
if( count_comments > padComment.length ) { | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
}, 9000); | ||
} | ||
}, 3000); | ||
} | ||
}, 1000); | ||
} | ||
}, 300); | ||
self.collectCommentsAfterSomeIntervalsOfTime(); | ||
}); | ||
@@ -129,19 +102,12 @@ | ||
self.localizeExistingComments(); | ||
newComment.localizeNewCommentForm(); | ||
}); | ||
// When screen size changes (user changes device orientation, for example), | ||
// we need to make sure all sidebar comments are on the correct place | ||
newComment.waitForResizeToFinishThenCall(200, function() { | ||
self.editorResized(); | ||
// Recalculate position when editor is resized | ||
$('#settings input, #skin-variant-full-width').on('change', function(e) { | ||
self.setYofComments(); | ||
}); | ||
// When Page View is enabled/disabled, we need to recalculate position of comments | ||
$('#options-pageview').on('click', function(e) { | ||
self.editorResized(); | ||
this.padInner.contents().on(UPDATE_COMMENT_LINE_POSITION_EVENT, function(e){ | ||
self.setYofComments(); | ||
}); | ||
// When Page Breaks are enabled/disabled, we need to recalculate position of comments | ||
$('#options-pagebreaks').on('click', function(e) { | ||
self.editorResized(); | ||
}); | ||
$(window).resize(_.debounce( function() { self.setYofComments() }, 100 ) ); | ||
@@ -154,99 +120,130 @@ // On click comment icon toolbar | ||
// Import for below listener : we are using this.container.parent() so we include | ||
// events on both comment-modal and sidebar | ||
// Listen for events to delete a comment | ||
// All this does is remove the comment attr on the selection | ||
this.container.on("click", ".comment-delete", function(){ | ||
var commentId = $(this).parent().parent()[0].id; | ||
this.container.parent().on("click", ".comment-delete", function(){ | ||
var commentId = $(this).closest('.comment-container')[0].id; | ||
self.deleteComment(commentId); | ||
}) | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
var selector = "."+commentId; | ||
var ace = self.ace; | ||
ace.callWithAce(function(aceTop){ | ||
var repArr = aceTop.ace_getRepFromSelector(selector, padInner); | ||
// rep is an array of reps.. I will need to iterate over each to do something meaningful.. | ||
$.each(repArr, function(index, rep){ | ||
// I don't think we need this nested call | ||
ace.callWithAce(function (ace){ | ||
ace.ace_performSelectionChange(rep[0],rep[1],true); | ||
ace.ace_setAttributeOnSelection('comment', 'comment-deleted'); | ||
// Note that this is the correct way of doing it, instead of there being | ||
// a commentId we now flag it as "comment-deleted" | ||
}); | ||
}); | ||
},'deleteCommentedSelection', true); | ||
// dispatch event | ||
self.socket.emit('deleteComment', {padId: self.padId, commentId: commentId}, function (){}); | ||
}); | ||
// Listen for include suggested change toggle | ||
this.container.on("change", '.reply-suggestion-checkbox', function(){ | ||
if($(this).is(':checked')){ | ||
var commentId = $(this).parent().parent().parent()[0].id; | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
// Listen for events to edit a comment | ||
// Here, it adds a form to edit the comment text | ||
this.container.parent().on("click", ".comment-edit", function(){ | ||
var $commentBox = $(this).closest('.comment-container'); | ||
$commentBox.addClass('editing'); | ||
var currentString = padInner.contents().find("."+commentId).html(); | ||
$(this).parent().parent().find(".reply-comment-changeFrom-value").html(currentString); | ||
$(this).parent().parent().find('.reply-suggestion').addClass("active"); | ||
}else{ | ||
$(this).parent().parent().find('.reply-suggestion').removeClass("active"); | ||
var textBox = self.findCommentText($commentBox).last(); | ||
// if edit form not already there | ||
if (textBox.siblings('.comment-edit-form').length == 0) { | ||
// add a form to edit the field | ||
var data = {}; | ||
data.text = textBox.text(); | ||
var content = $("#editCommentTemplate").tmpl(data); | ||
// localize the comment/reply edit form | ||
commentL10n.localize(content); | ||
// insert form | ||
textBox.before(content); | ||
} | ||
}); | ||
// Create hover modal | ||
$('iframe[name="ace_outer"]').contents().find("body") | ||
.append("<div class='comment-modal'><p class='comment-modal-name'></p><p class='comment-modal-comment'></p></div>"); | ||
// DUPLICATE CODE REQUIRED FOR COMMENT REPLIES, see below for slightly different version | ||
this.container.on("click", ".comment-reply-changeTo-approve > input", function(e){ | ||
// submit the edition on the text and update the comment text | ||
this.container.parent().on("click", ".comment-edit-submit", function(e){ | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
var $commentBox = $(this).closest('.comment-container'); | ||
var $commentForm = $(this).closest('.comment-edit-form'); | ||
var commentId = $commentBox.data('commentid'); | ||
var commentText = $commentForm.find('.comment-edit-text').val(); | ||
var data = {}; | ||
data.commentId = $(this).parent().parent().parent().parent().parent()[0].id; | ||
data.commentId = commentId; | ||
data.padId = clientVars.padId; | ||
data.commentText = commentText; | ||
data.replyId = $(this).parent().parent().parent()[0].id; | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
self.socket.emit('updateCommentText', data, function (err){ | ||
if(!err) { | ||
$commentForm.remove(); | ||
$commentBox.removeClass('editing'); | ||
self.updateCommentBoxText(commentId, commentText); | ||
// Are we reverting a change? | ||
var submitButton = $(this); | ||
var isRevert = submitButton.hasClass("revert"); | ||
if(isRevert){ | ||
var newString = $(this).parent().parent().parent().contents().find(".comment-changeFrom-value").html(); | ||
}else{ | ||
var newString = $(this).parent().parent().parent().contents().find(".comment-changeTo-value").html(); | ||
} | ||
// although the comment or reply was saved on the data base successfully, it needs | ||
// to update the comment or comment reply variable with the new text saved | ||
self.setCommentOrReplyNewText(commentId, commentText); | ||
} | ||
}); | ||
}); | ||
// Nuke all that aren't first lines of this comment | ||
padInner.contents().find("."+data.commentId+":not(:first)").html(""); | ||
var padCommentContent = padInner.contents().find("."+data.commentId).first(); | ||
newString = newString.replace(/(?:\r\n|\r|\n)/g, '<br />'); | ||
// hide the edit form and make the comment author and text visible again | ||
this.container.parent().on("click", ".comment-edit-cancel", function(e){ | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
var $commentBox = $(this).closest('.comment-container'); | ||
var textBox = self.findCommentText($commentBox).last(); | ||
textBox.siblings('.comment-edit-form').remove(); | ||
$commentBox.removeClass('editing'); | ||
}); | ||
// Write the new pad contents | ||
$(padCommentContent).html(newString); | ||
// Listen for include suggested change toggle | ||
this.container.parent().on("change", '.suggestion-checkbox', function(){ | ||
var parentComment = $(this).closest('.comment-container'); | ||
var parentSuggest = $(this).closest('.comment-reply'); | ||
// We change commentId to replyId in the data object so it's properly processed by the server.. This is hacky | ||
data.commentId = data.replyId; | ||
if($(this).is(':checked')){ | ||
var commentId = parentComment.data('commentid'); | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
if(isRevert){ | ||
// Tell all users this change was reverted | ||
self.socket.emit('revertChange', data, function (){}); | ||
self.showChangeAsReverted(data.replyId); | ||
var currentString = padInner.contents().find("."+commentId).html(); | ||
parentSuggest.find(".from-value").html(currentString); | ||
parentSuggest.find('.suggestion').show(); | ||
}else{ | ||
// Tell all users this change was accepted | ||
self.socket.emit('acceptChange', data, function (){}); | ||
// Update our own comments container with the accepted change | ||
self.showChangeAsAccepted(data.replyId); | ||
parentSuggest.find('.suggestion').hide(); | ||
} | ||
}); | ||
// User accepts a change | ||
this.container.on("submit", ".comment-changeTo-form", function(e){ | ||
// User accepts or revert a change | ||
this.container.parent().on("submit", ".comment-changeTo-form", function(e){ | ||
e.preventDefault(); | ||
var data = self.getCommentData(); | ||
data.commentId = $(this).parent()[0].id; | ||
var commentEl = $(this).closest('.comment-container'); | ||
data.commentId = commentEl.data('commentid'); | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]').contents(); | ||
// Are we reverting a change? | ||
var submitButton = $(this).contents().find("input[type='submit']"); | ||
var isRevert = submitButton.hasClass("revert"); | ||
if(isRevert){ | ||
var newString = $(this).parent().contents().find(".comment-changeFrom-value").html(); | ||
}else{ | ||
var newString = $(this).parent().contents().find(".comment-changeTo-value").html(); | ||
} | ||
var isRevert = commentEl.hasClass("change-accepted"); | ||
var newString = isRevert ? $(this).find(".from-value").html() : $(this).find(".to-value").html(); | ||
// In case of suggested change is inside a reply, the parentId is different from the commentId (=replyId) | ||
var parentId = $(this).closest('.sidebar-comment').data('commentid'); | ||
// Nuke all that aren't first lines of this comment | ||
padInner.contents().find("."+data.commentId+":not(:first)").html(""); | ||
padInner.find("."+parentId+":not(:first)").html(""); | ||
var padCommentContent = padInner.contents().find("."+data.commentId).first(); | ||
var padCommentSpan = padInner.find("."+parentId).first(); | ||
newString = newString.replace(/(?:\r\n|\r)/g, '<br />'); | ||
// Write the new pad contents | ||
$(padCommentContent).html(newString); | ||
padCommentSpan.html(newString); | ||
@@ -260,78 +257,105 @@ if(isRevert){ | ||
self.socket.emit('acceptChange', data, function (){}); | ||
// Update our own comments container with the accepted change | ||
self.showChangeAsAccepted(data.commentId); | ||
} | ||
// TODO: we need ace editor to commit the change so other people get it | ||
// currently after approving or reverting, you need to do other thing on the pad | ||
// for ace to commit | ||
}); | ||
// is this even used? - Yes, it is! | ||
this.container.on("submit", ".comment-reply", function(e){ | ||
// When input reply is focused we display more option | ||
this.container.parent().on("focus", ".comment-content", function(e){ | ||
$(this).closest('.new-comment').addClass('editing'); | ||
}); | ||
// When we leave we reset the form option to its minimal (only input) | ||
this.container.parent().on('mouseleave', ".comment-container", function(e) { | ||
$(this).find('.suggestion-checkbox').prop('checked', false); | ||
$(this).find('.new-comment').removeClass('editing'); | ||
}); | ||
// When a reply get submitted | ||
this.container.parent().on("submit", ".new-comment", function(e){ | ||
e.preventDefault(); | ||
var data = self.getCommentData(); | ||
data.commentId = $(this).parent()[0].id; | ||
data.reply = $(this).find(".comment-reply-input").val(); | ||
data.changeTo = $(this).find(".reply-comment-suggest-to").val() || null; | ||
data.changeFrom = $(this).find(".reply-comment-changeFrom-value").text() || null; | ||
data.commentId = $(this).closest('.comment-container').data('commentid'); | ||
data.reply = $(this).find(".comment-content").val(); | ||
data.changeTo = $(this).find(".to-value").val() || null; | ||
data.changeFrom = $(this).find(".from-value").text() || null; | ||
self.socket.emit('addCommentReply', data, function (){ | ||
// Append the reply to the comment | ||
// console.warn("addCommentReplyEmit WE EXPECT REPLY ID", data); | ||
$('iframe[name="ace_outer"]').contents().find('#'+data.commentId + ' > form.comment-reply .comment-reply-input').val(""); | ||
self.getCommentReplies(function(replies){ | ||
self.commentReplies = replies; | ||
self.collectCommentReplies(); | ||
// Once the new reply is displayed, we clear the form | ||
$('iframe[name="ace_outer"]').contents().find('.new-comment').removeClass('editing'); | ||
}); | ||
}); | ||
// On submit we should hide this suggestion no? | ||
if($(this).parent().parent().find(".reply-suggestion-checkbox").is(':checked')){ | ||
$(this).parent().parent().find(".reply-suggestion-checkbox:checked").click(); | ||
$(this).parent().parent().find(".reply-comment-suggest-to").val(""); | ||
//Only uncheck checked boxes. TODO: is a cleanup operation. Should we do it here? | ||
} | ||
$(this).trigger('reset_reply'); | ||
}); | ||
this.container.parent().on("reset_reply", ".new-comment", function(e){ | ||
// Reset the form | ||
$(this).find('.comment-content').val(''); | ||
$(this).find(':focus').blur(); | ||
$(this).find('.to-value').val(''); | ||
$(this).find('.suggestion-checkbox').prop('checked', false); | ||
$(this).removeClass('editing'); | ||
}); | ||
// When click cancel reply | ||
this.container.parent().on("click", ".btn-cancel-reply", function(e) { | ||
$(this).closest('.new-comment').trigger('reset_reply') | ||
}); | ||
// Enable and handle cookies | ||
if (padcookie.getPref("comments") === false) { | ||
self.container.removeClass("active"); | ||
self.padOuter.find('#comments, #commentIcons').removeClass("active"); | ||
$('#options-comments').attr('checked','unchecked'); | ||
$('#options-comments').attr('checked',false); | ||
}else{ | ||
} else { | ||
$('#options-comments').attr('checked','checked'); | ||
} | ||
$('#options-comments').on('click', function() { | ||
if($('#options-comments').is(':checked')) { | ||
padcookie.setPref("comments", true); | ||
self.container.addClass("active"); | ||
} else { | ||
padcookie.setPref("comments", false); | ||
self.container.removeClass("active"); | ||
} | ||
$('#options-comments').on('change', function() { | ||
$('#options-comments').is(':checked') ? enableComments() : disableComments(); | ||
}); | ||
// Check to see if we should show already.. | ||
if($('#options-comments').is(':checked')){ | ||
self.container.addClass("active"); | ||
function enableComments() { | ||
padcookie.setPref("comments", true); | ||
self.padOuter.find('#comments, #commentIcons').addClass("active"); | ||
$('body').addClass('comments-active') | ||
$('iframe[name="ace_outer"]').contents().find('body').addClass('comments-active') | ||
} | ||
// Override copy, cut, paste events on Google chrome. | ||
function disableComments() { | ||
padcookie.setPref("comments", false); | ||
self.padOuter.find('#comments, #commentIcons').removeClass("active"); | ||
$('body').removeClass('comments-active') | ||
$('iframe[name="ace_outer"]').contents().find('body').removeClass('comments-active') | ||
} | ||
// Check to see if we should show already.. | ||
$('#options-comments').trigger('change'); | ||
// TODO - Implement to others browser like, Microsoft Edge, Opera, IE | ||
// Override copy, cut, paste events on Google chrome and Mozilla Firefox. | ||
// When an user copies a comment and selects only the span, or part of it, Google chrome | ||
// does not copy the classes only the styles, for example: | ||
// <comment><span>text to be copied</span></comment> | ||
// <comment class='comment'><span>text to be copied</span></comment> | ||
// As the comment classes are not only used for styling we have to add these classes when it pastes the content | ||
// The same does not occur when the user selects more than the span, for example: | ||
// text<comment><span>to be copied</span></comment> | ||
if(browser.chrome){ | ||
// text<comment class='comment'><span>to be copied</span></comment> | ||
if(browser.chrome || browser.firefox){ | ||
self.padInner.contents().on("copy", function(e) { | ||
events.addTextOnClipboard(e, self.ace, self.padInner); | ||
events.addTextOnClipboard(e, self.ace, self.padInner, false, self.comments, self.commentReplies); | ||
}); | ||
self.padInner.contents().on("cut", function(e) { | ||
events.addTextOnClipboard(e, self.ace, self.padInner); | ||
// remove the selected text | ||
self.padInner.contents()[0].execCommand("delete"); | ||
events.addTextOnClipboard(e, self.ace, self.padInner, true); | ||
}); | ||
self.padInner.contents().on("paste", function(e) { | ||
events.addCommentClasses(e); | ||
events.saveCommentsAndReplies(e); | ||
}); | ||
@@ -341,2 +365,46 @@ } | ||
ep_comments.prototype.findCommentText = function($commentBox) { | ||
var isReply = $commentBox.hasClass('sidebar-comment-reply') | ||
if (isReply) | ||
return $commentBox.find(".comment-text"); | ||
else | ||
return $commentBox.find('.compact-display-content .comment-text, .full-display-content .comment-title-wrapper .comment-text'); | ||
} | ||
// This function is useful to collect new comments on the collaborators | ||
ep_comments.prototype.collectCommentsAfterSomeIntervalsOfTime = function() { | ||
var self = this; | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
var count_comments=0; | ||
for(var key in self.comments) {count_comments++;} | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
this.padOuter = padOuter; | ||
this.padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
var padComment = this.padInner.contents().find('.comment'); | ||
if( count_comments > padComment.length ) { | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
var count_comments=0; | ||
for(var key in self.comments) {count_comments++;} | ||
var padComment = this.padInner.contents().find('.comment'); | ||
if( count_comments > padComment.length ) { | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
var count_comments=0; | ||
for(var key in self.comments) {count_comments++;} | ||
var padComment = this.padInner.contents().find('.comment'); | ||
if( count_comments > padComment.length ) { | ||
window.setTimeout(function() { | ||
self.collectComments(); | ||
}, 9000); | ||
} | ||
}, 3000); | ||
} | ||
}, 1000); | ||
} | ||
}, 300); | ||
} | ||
// Insert comments container on element use for linenumbers | ||
@@ -362,3 +430,2 @@ ep_comments.prototype.findContainers = function(){ | ||
var commentId = (classCommentId) ? classCommentId[1] : null; | ||
if(!commentId){ | ||
@@ -385,12 +452,3 @@ // console.log("returning due to no comment id, probably due to a deleted comment"); | ||
self.insertComment(commentId, comment.data, it); | ||
commentElm = container.find('#'+ commentId); | ||
$(this).on('click', function(){ | ||
markerTop = $(this).position().top; | ||
commentTop = commentElm.position().top; | ||
containerTop = container.css('top'); | ||
container.css('top', containerTop - (commentTop - markerTop)); | ||
}); | ||
} | ||
// localize comment element | ||
@@ -413,34 +471,26 @@ commentL10n.localize(commentElm); | ||
commentElm.css({ 'top': commentPos }); | ||
}); | ||
// Should we show "Revert" instead of "Accept" | ||
// Comment Replies are NOT handled here.. | ||
if(comments[commentId]){ | ||
var showRevert = comments[commentId].data.changeAccepted; | ||
} | ||
// HOVER SIDEBAR COMMENT | ||
var hideCommentTimer; | ||
this.container.on("mouseover", ".sidebar-comment", function(e){ | ||
// highlight comment | ||
clearTimeout(hideCommentTimer); | ||
commentBoxes.highlightComment(e.currentTarget.id, e); | ||
if(showRevert){ | ||
self.showChangeAsAccepted(commentId); | ||
} | ||
}); | ||
// now if we apply a class such as mouseover to the editor it will go shitty | ||
// so what we need to do is add CSS for the specific ID to the document... | ||
// It's fucked up but that's how we do it.. | ||
var padInner = this.padInner; | ||
this.container.on("mouseover", ".sidebar-comment", function(e){ | ||
var commentId = e.currentTarget.id; | ||
var inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); | ||
inner.contents().find("head").append("<style>."+commentId+"{ color:orange }</style>"); | ||
// on hover we should show the reply option | ||
}).on("mouseout", ".sidebar-comment", function(e){ | ||
var commentId = e.currentTarget.id; | ||
var inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); | ||
inner.contents().find("head").append("<style>."+commentId+"{ color:black }</style>"); | ||
// TODO this could potentially break ep_font_color | ||
// do not hide directly the comment, because sometime the mouse get out accidently | ||
hideCommentTimer = setTimeout(function() { | ||
commentBoxes.hideComment(e.currentTarget.id); | ||
},1000); | ||
}); | ||
// HOVER OR CLICK THE COMMENTED TEXT IN THE EDITOR | ||
// hover event | ||
this.padInner.contents().on("mouseover", ".comment", function(e){ | ||
var commentId = self.commentIdOf(e); | ||
commentBoxes.highlightComment(commentId, e); | ||
if (container.is(':visible')) { // not on mobile | ||
clearTimeout(hideCommentTimer); | ||
var commentId = self.commentIdOf(e); | ||
commentBoxes.highlightComment(commentId, e, $(this)); | ||
} | ||
}); | ||
@@ -451,3 +501,3 @@ | ||
var commentId = self.commentIdOf(e); | ||
commentBoxes.highlightComment(commentId, e); | ||
commentBoxes.highlightComment(commentId, e, $(this)); | ||
}); | ||
@@ -457,6 +507,7 @@ | ||
var commentOpenedByClickOnIcon = commentIcons.isCommentOpenedByClickOnIcon(); | ||
// only closes comment if it was not opened by a click on the icon | ||
if (!commentOpenedByClickOnIcon) { | ||
self.closeOpenedComment(e); | ||
if (!commentOpenedByClickOnIcon && container.is(':visible')) { | ||
hideCommentTimer = setTimeout(function() { | ||
self.closeOpenedComment(e); | ||
}, 1000); | ||
} | ||
@@ -468,2 +519,3 @@ }); | ||
self.setYofComments(); | ||
if(callback) callback(); | ||
}; | ||
@@ -475,9 +527,9 @@ | ||
// we need to add listeners to the different iframes of the page | ||
$(document).on("touchstart", function(e){ | ||
$(document).on("touchstart click", function(e){ | ||
self.closeOpenedCommentIfNotOnSelectedElements(e); | ||
}); | ||
this.padOuter.find('html').on("touchstart", function(e){ | ||
this.padOuter.find('html').on("touchstart click", function(e){ | ||
self.closeOpenedCommentIfNotOnSelectedElements(e); | ||
}); | ||
this.padInner.contents().find('html').on("touchstart", function(e){ | ||
this.padInner.contents().find('html').on("touchstart click", function(e){ | ||
self.closeOpenedCommentIfNotOnSelectedElements(e); | ||
@@ -500,3 +552,2 @@ }); | ||
} | ||
// All clear, can close the comment | ||
@@ -512,5 +563,5 @@ this.closeOpenedComment(e); | ||
var padComment = this.padInner.contents().find('.comment'); | ||
$.each(this.commentReplies, function(replyId, reply){ | ||
var commentId = reply.commentId; | ||
// tell comment icon that this comment has 1+ replies | ||
@@ -520,7 +571,7 @@ commentIcons.commentHasReply(commentId); | ||
var existsAlready = $('iframe[name="ace_outer"]').contents().find('#'+replyId).length; | ||
if(existsAlready){ | ||
return; | ||
} | ||
if(existsAlready) return; | ||
reply.replyId = replyId; | ||
reply.text = reply.text || "" | ||
reply.date = prettyDate(reply.timestamp); | ||
reply.formattedDate = new Date(reply.timestamp).toISOString(); | ||
@@ -531,8 +582,4 @@ | ||
commentL10n.localize(content); | ||
$('iframe[name="ace_outer"]').contents().find('#'+commentId + ' .comment-reply-input-label').before(content); | ||
// Should we show "Revert" instead of "Accept" | ||
// Comment Replies ARE handled here.. | ||
if(reply.changeAccepted){ | ||
self.showChangeAsAccepted(replyId); | ||
} | ||
var repliesContainer = $('iframe[name="ace_outer"]').contents().find('#'+commentId + ' .comment-replies-container'); | ||
repliesContainer.append(content); | ||
}); | ||
@@ -552,8 +599,9 @@ }; | ||
// Add comments | ||
// Create hover modal | ||
target.prepend("<div class='comment-modal popup'><div class='popup-content comment-modal-comment'></div></div>"); | ||
// Add comments side bar container | ||
target.prepend('<div id="comments"></div>'); | ||
this.container = this.padOuter.find('#comments'); | ||
// Add newComments | ||
newComment.insertContainers(target); | ||
}; | ||
@@ -568,2 +616,3 @@ | ||
comment.commentId = commentId; | ||
comment.reply = true; | ||
content = $('#commentsTemplate').tmpl(comment); | ||
@@ -593,3 +642,3 @@ | ||
var padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
var inlineComments = padInner.contents().find(".comment"); | ||
var inlineComments = this.getFirstOcurrenceOfCommentIds(); | ||
var commentsToBeShown = []; | ||
@@ -603,9 +652,15 @@ | ||
$.each(inlineComments, function(){ | ||
var y = this.offsetTop; | ||
var commentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(this.className); // classname is the ID of the comment | ||
var commentEle = padOuter.find('#'+commentId[1]) | ||
var topOffset = this.offsetTop; | ||
topOffset += parseInt(padInner.css('padding-top').split('px')[0]) | ||
// topOffset += ($(this).height() + $(this).outerHeight(true)) / 2 - commentEle.height(); | ||
if(commentId) { | ||
// adjust outer comment... | ||
var commentEle = commentBoxes.adjustTopOf(commentId[1], y); | ||
commentBoxes.adjustTopOf(commentId[1], topOffset); | ||
// ... and adjust icons too | ||
commentIcons.adjustTopOf(commentId[1], y); | ||
commentIcons.adjustTopOf(commentId[1], topOffset); | ||
@@ -623,27 +678,22 @@ // mark this comment to be displayed if it was visible before we start adjusting its position | ||
// Make the adjustments after editor is resized (due to a window resize or | ||
// enabling/disabling Page View) | ||
ep_comments.prototype.editorResized = function() { | ||
var self = this; | ||
ep_comments.prototype.getFirstOcurrenceOfCommentIds = function(){ | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]').contents(); | ||
var commentsId = this.getUniqueCommentsId(padInner); | ||
var firstOcurrenceOfCommentIds = _.map(commentsId, function(commentId){ | ||
return padInner.find("." + commentId).first().get(0); | ||
}); | ||
return firstOcurrenceOfCommentIds; | ||
} | ||
commentIcons.adjustIconsForNewScreenSize(); | ||
// We try increasing timeouts, to make sure user gets the response as fast as we can | ||
setTimeout(function() { | ||
if (!self.allCommentsOnCorrectYPosition()) self.adjustCommentPositions(); | ||
setTimeout(function() { | ||
if (!self.allCommentsOnCorrectYPosition()) self.adjustCommentPositions(); | ||
setTimeout(function() { | ||
if (!self.allCommentsOnCorrectYPosition()) self.adjustCommentPositions(); | ||
}, 1000); | ||
}, 500); | ||
}, 250); | ||
ep_comments.prototype.getUniqueCommentsId = function(padInner){ | ||
var inlineComments = padInner.find(".comment"); | ||
var commentsId = _.map(inlineComments, function(inlineComment){ | ||
var commentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(inlineComment.className); | ||
// avoid when it has a '.comment' that it has a fakeComment class 'fakecomment-123' yet. | ||
if(commentId) return commentId[1]; | ||
}); | ||
return _.uniq(commentsId); | ||
} | ||
// Adjusts position on the screen for sidebar comments and comment icons | ||
ep_comments.prototype.adjustCommentPositions = function(){ | ||
commentIcons.adjustIconsForNewScreenSize(); | ||
this.setYofComments(); | ||
} | ||
// Indicates if all comments are on the correct Y position, and don't need to | ||
@@ -692,3 +742,2 @@ // be adjusted | ||
comment.data.formattedDate = new Date(comment.data.timestamp).toISOString(); | ||
commentElm.attr('title', comment.data.date); | ||
} | ||
@@ -710,6 +759,25 @@ }); | ||
comment.formattedDate = new Date(comment.timestamp).toISOString(); | ||
if (comments[commentId] == null) comments[commentId] = {}; | ||
comments[commentId].data = comment; | ||
}; | ||
// commentReply = ['c-reply-123', commentDataObject] | ||
// commentDataObject = {author:..., name:..., text:..., ...} | ||
ep_comments.prototype.setCommentReply = function(commentReply){ | ||
var commentReplies = this.commentReplies; | ||
var replyId = commentReply[0]; | ||
commentReplies[replyId] = commentReply[1]; | ||
}; | ||
// set the text of the comment or comment reply | ||
ep_comments.prototype.setCommentOrReplyNewText = function(commentOrReplyId, text){ | ||
if(this.comments[commentOrReplyId]){ | ||
this.comments[commentOrReplyId].data.text = text; | ||
}else if(this.commentReplies[commentOrReplyId]){ | ||
this.commentReplies[commentOrReplyId].text = text; | ||
} | ||
}; | ||
// Get all comments | ||
@@ -753,22 +821,3 @@ ep_comments.prototype.getComments = function (callback){ | ||
ep_comments.prototype.deleteComment = function(commentId){ | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
var padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
var selector = "."+commentId; | ||
var ace = this.ace; | ||
ace.callWithAce(function(aceTop){ | ||
var repArr = aceTop.ace_getRepFromSelector(selector, padInner); | ||
// rep is an array of reps.. I will need to iterate over each to do something meaningful.. | ||
$.each(repArr, function(index, rep){ | ||
// I don't think we need this nested call | ||
ace.callWithAce(function (ace){ | ||
ace.ace_performSelectionChange(rep[0],rep[1],true); | ||
ace.ace_setAttributeOnSelection('comment', 'comment-deleted'); | ||
// Note that this is the correct way of doing it, instead of there being | ||
// a commentId we now flag it as "comment-deleted" | ||
}); | ||
}); | ||
},'deleteCommentedSelection', true); | ||
// }); | ||
// }, 'getRep'); | ||
$('iframe[name="ace_outer"]').contents().find('#' + commentId).remove(); | ||
} | ||
@@ -793,2 +842,3 @@ | ||
if (noTextSelected) { | ||
$.gritter.add({text: html10n.translations["ep_comments_page.add_comment.hint"] || "Please first select the text to comment"}) | ||
return; | ||
@@ -800,7 +850,6 @@ } | ||
// Write the text to the changeFrom form | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
padOuter.find(".comment-suggest-from").val(selectedText); | ||
$('#newComment').find(".from-value").text(selectedText); | ||
// Display form | ||
newComment.showNewCommentForm(); | ||
newComment.showNewCommentPopup(); | ||
@@ -816,11 +865,3 @@ // Check if the first element selected is visible in the viewport | ||
// Adjust focus on the form | ||
padOuter.find('.comment-content').focus(); | ||
// fix for iOS: when opening #newComment, we need to force focus on padOuter | ||
// contentWindow, otherwise keyboard will be displayed but text input made by | ||
// the user won't be added to textarea | ||
var outerIframe = $('iframe[name="ace_outer"]').get(0); | ||
if (outerIframe && outerIframe.contentWindow) { | ||
outerIframe.contentWindow.focus(); | ||
} | ||
$('#newComment').find('.comment-content').focus(); | ||
} | ||
@@ -879,3 +920,3 @@ | ||
ep_comments.prototype.checkNoTextSelected = function(rep) { | ||
var noTextSelected = (rep.selStart[0] == rep.selEnd[0] && rep.selStart[1] == rep.selEnd[1]); | ||
var noTextSelected = ((rep.selStart[0] == rep.selEnd[0]) && (rep.selStart[1] == rep.selEnd[1])); | ||
@@ -891,3 +932,3 @@ return noTextSelected; | ||
// If a new comment box doesn't already exist, create one | ||
newComment.insertNewCommentFormIfDontExist(data, function(comment, index) { | ||
newComment.insertNewCommentPopupIfDontExist(data, function(comment, index) { | ||
if(comment.changeTo){ | ||
@@ -987,3 +1028,2 @@ data.comment.changeFrom = comment.changeFrom; | ||
var self = this; | ||
self.socket.emit('addComment', data, function (commentId, comment){ | ||
@@ -993,3 +1033,3 @@ comment.commentId = commentId; | ||
self.ace.callWithAce(function (ace){ | ||
// console.log('addComment :: ', commentId); | ||
// console.log('addComment :: ', rep, comment); | ||
ace.ace_performSelectionChange(rep.selStart, rep.selEnd, true); | ||
@@ -1004,2 +1044,95 @@ ace.ace_setAttributeOnSelection('comment', commentId); | ||
// commentData = {c-newCommentId123: data:{author:..., date:..., ...}, c-newCommentId124: data:{...}} | ||
ep_comments.prototype.saveCommentWithoutSelection = function (padId, commentData) { | ||
var self = this; | ||
var data = self.buildComments(commentData); | ||
self.socket.emit('bulkAddComment', padId, data, function (comments){ | ||
self.setComments(comments); | ||
self.shouldCollectComment = true | ||
}); | ||
} | ||
ep_comments.prototype.buildComments = function(commentsData){ | ||
var self = this; | ||
var comments = _.map(commentsData, function(commentData, commentId){ | ||
return self.buildComment(commentId, commentData.data); | ||
}); | ||
return comments; | ||
} | ||
// commentData = {c-newCommentId123: data:{author:..., date:..., ...}, ... | ||
ep_comments.prototype.buildComment = function(commentId, commentData){ | ||
var data = {}; | ||
data.padId = this.padId; | ||
data.commentId = commentId; | ||
data.text = commentData.text; | ||
data.changeTo = commentData.changeTo | ||
data.changeFrom = commentData.changeFrom; | ||
data.name = commentData.name; | ||
data.timestamp = parseInt(commentData.timestamp); | ||
return data; | ||
} | ||
ep_comments.prototype.getMapfakeComments = function(){ | ||
return this.mapFakeComments; | ||
} | ||
// commentReplyData = {c-reply-123:{commentReplyData1}, c-reply-234:{commentReplyData1}, ...} | ||
ep_comments.prototype.saveCommentReplies = function(padId, commentReplyData){ | ||
var self = this; | ||
var data = self.buildCommentReplies(commentReplyData); | ||
self.socket.emit('bulkAddCommentReplies', padId, data, function (replies){ | ||
_.each(replies,function(reply){ | ||
self.setCommentReply(reply); | ||
}); | ||
self.shouldCollectComment = true; // force collect the comment replies saved | ||
}); | ||
} | ||
ep_comments.prototype.buildCommentReplies = function(repliesData){ | ||
var self = this; | ||
var replies = _.map(repliesData, function(replyData){ | ||
return self.buildCommentReply(replyData); | ||
}); | ||
return replies; | ||
} | ||
// take a replyData and add more fields necessary. E.g. 'padId' | ||
ep_comments.prototype.buildCommentReply = function(replyData){ | ||
var data = {}; | ||
data.padId = this.padId; | ||
data.commentId = replyData.commentId; | ||
data.text = replyData.text; | ||
data.changeTo = replyData.changeTo | ||
data.changeFrom = replyData.changeFrom; | ||
data.replyId = replyData.replyId; | ||
data.name = replyData.name; | ||
data.timestamp = parseInt(replyData.timestamp); | ||
return data; | ||
} | ||
// Listen for comment | ||
ep_comments.prototype.commentListen = function(){ | ||
var self = this; | ||
var socket = this.socket; | ||
socket.on('pushAddCommentInBulk', function (){ | ||
self.getComments(function (allComments){ | ||
if (!$.isEmptyObject(allComments)){ | ||
// we get the comments in this format {c-123:{author:...}, c-124:{author:...}} | ||
// but it's expected to be {c-123: {data: {author:...}}, c-124:{data:{author:...}}} | ||
// in this.comments | ||
var commentsProcessed = {}; | ||
_.map(allComments, function (comment, commentId) { | ||
commentsProcessed[commentId] = {} | ||
commentsProcessed[commentId].data = comment; | ||
}); | ||
self.comments = commentsProcessed; | ||
self.collectCommentsAfterSomeIntervalsOfTime(); // here we collect on the collaborators | ||
} | ||
}); | ||
}); | ||
}; | ||
// Listen for comment replies | ||
@@ -1018,3 +1151,2 @@ ep_comments.prototype.commentRepliesListen = function(){ | ||
self.collectCommentReplies(); | ||
self.commentRepliesListen(); | ||
} | ||
@@ -1026,2 +1158,8 @@ }); | ||
ep_comments.prototype.updateCommentBoxText = function (commentId, commentText) { | ||
var $comment = this.container.parent().find("[data-commentid='" + commentId + "']"); | ||
var textBox = this.findCommentText($comment); | ||
textBox.text(commentText) | ||
} | ||
ep_comments.prototype.showChangeAsAccepted = function(commentId){ | ||
@@ -1031,7 +1169,14 @@ var self = this; | ||
// Get the comment | ||
var comment = self.container.find("#"+commentId); | ||
var button = comment.find("input[type='submit']").first(); // we need to get the first button otherwise the replies suggestions will be affected too | ||
button.attr("data-l10n-id", "ep_comments_page.comments_template.revert_change.value"); | ||
button.addClass("revert"); | ||
commentL10n.localize(button); | ||
var comment = this.container.parent().find("[data-commentid='" + commentId + "']"); | ||
// Revert other comment that have already been accepted | ||
comment.closest('.sidebar-comment') | ||
.find('.comment-container.change-accepted').addBack('.change-accepted') | ||
.each(function() { | ||
$(this).removeClass('change-accepted'); | ||
var data = {commentId: $(this).attr('data-commentid'), padId: self.padId} | ||
self.socket.emit('revertChange', data, function (){}); | ||
}) | ||
// this comment get accepted | ||
comment.addClass('change-accepted'); | ||
} | ||
@@ -1041,9 +1186,5 @@ | ||
var self = this; | ||
// Get the comment | ||
var comment = self.container.find("#"+commentId); | ||
var button = comment.find("input[type='submit']").first(); // we need to get the first button otherwise the replies suggestions will be affected too | ||
button.attr("data-l10n-id", "ep_comments_page.comments_template.accept_change.value"); | ||
button.removeClass("revert"); | ||
commentL10n.localize(button); | ||
var comment = self.container.parent().find("[data-commentid='" + commentId + "']"); | ||
comment.removeClass('change-accepted'); | ||
} | ||
@@ -1056,2 +1197,10 @@ | ||
socket.on('textCommentUpdated', function (commentId, commentText) { | ||
self.updateCommentBoxText(commentId, commentText); | ||
}) | ||
socket.on('commentDeleted', function(commentId){ | ||
self.deleteComment(commentId); | ||
}); | ||
socket.on('changeAccepted', function(commentId){ | ||
@@ -1072,9 +1221,2 @@ self.showChangeAsAccepted(commentId); | ||
// On collaborator delete a comment in the current pad | ||
else if (eventType == 'remove'){ | ||
socket.on('pushRemoveComment', function (commentId){ | ||
callback(commentId); | ||
}); | ||
} | ||
// On reply | ||
@@ -1099,2 +1241,11 @@ else if (eventType == "addCommentReply"){ | ||
pad.plugins.ep_comments_page = Comments; | ||
if (!$('#editorcontainerbox').hasClass('flex-layout')) { | ||
$.gritter.add({ | ||
title: "Error", | ||
text: "Ep_comments_page: Please upgrade to etherpad 1.8.3 for this plugin to work correctly", | ||
sticky: true, | ||
class_name: "error" | ||
}) | ||
} | ||
}, | ||
@@ -1113,8 +1264,12 @@ | ||
// padOuter.find('#sidediv').removeClass("sidedivhidden"); // TEMPORARY to do removing authorship colors can add sidedivhidden class to sidesiv! | ||
if(!context.callstack.docTextChanged) return; | ||
// only adjust comments if plugin was already initialized, | ||
// otherwise there's nothing to adjust anyway | ||
if (pad.plugins && pad.plugins.ep_comments_page) { | ||
pad.plugins.ep_comments_page.setYofComments(); | ||
if(eventType == "setup" || eventType == "setBaseText" || eventType == "importText") return; | ||
if(context.callstack.docTextChanged) pad.plugins.ep_comments_page.setYofComments(); | ||
var commentWasPasted = pad.plugins.ep_comments_page.shouldCollectComment; | ||
var domClean = context.callstack.domClean; | ||
// we have to wait the DOM update from a fakeComment 'fakecomment-123' to a comment class 'c-123' | ||
if(commentWasPasted && domClean){ | ||
pad.plugins.ep_comments_page.collectComments(function(){ | ||
pad.plugins.ep_comments_page.collectCommentReplies(); | ||
pad.plugins.ep_comments_page.shouldCollectComment = false; | ||
}); | ||
} | ||
@@ -1239,4 +1394,5 @@ }, | ||
editorInfo.ace_getRepFromSelector = _(getRepFromSelector).bind(context); | ||
editorInfo.ace_getCommentIdOnSelection = _(getCommentIdOnSelection).bind(context); | ||
editorInfo.ace_getCommentIdOnFirstPositionSelected = _(getCommentIdOnFirstPositionSelected).bind(context); | ||
editorInfo.ace_hasCommentOnSelection = _(hasCommentOnSelection).bind(context); | ||
} | ||
var $ = require('ep_etherpad-lite/static/js/rjquery').$; | ||
var commentL10n = require('ep_comments_page/static/js/commentL10n'); | ||
// Easier access to outer pad | ||
var padOuter; | ||
var getPadOuter = function() { | ||
padOuter = padOuter || $('iframe[name="ace_outer"]').contents(); | ||
return padOuter; | ||
} | ||
// Easier access to new comment container | ||
var newCommentContainer; | ||
var getNewCommentContainer = function() { | ||
newCommentContainer = newCommentContainer || getPadOuter().find('#newComments'); | ||
return newCommentContainer; | ||
} | ||
// Insert a comment node | ||
var createNewCommentForm = function(comment) { | ||
var container = getNewCommentContainer(); | ||
comment.commentId = ""; | ||
var content = $('#newCommentTemplate').tmpl(comment); | ||
content.prependTo(container); | ||
return content; | ||
}; | ||
// Create a comment object with data filled on the given form | ||
var buildCommentFrom = function(form) { | ||
var text = form.find('.comment-content').val(); | ||
var changeFrom = form.find('.comment-suggest-from').val(); | ||
var changeTo = form.find('.comment-suggest-to').val() || null; | ||
var changeFrom = form.find('.from-value').text(); | ||
var changeTo = form.find('.to-value').val() || null; | ||
var comment = {}; | ||
@@ -47,14 +22,18 @@ | ||
var cancelNewComment = function(){ | ||
hideNewCommentForm(); | ||
hideNewCommentPopup(); | ||
} | ||
// Callback for new comment Submit | ||
var submitNewComment = function(form, callback) { | ||
var submitNewComment = function(callback) { | ||
var index = 0; | ||
var text = form.find('.comment-content').val(); | ||
var commentTextIsNotEmpty = text.length !== 0; | ||
var form = $('#newComment'); | ||
var comment = buildCommentFrom(form); | ||
if (commentTextIsNotEmpty) { | ||
hideNewCommentForm(); | ||
if (comment.text.length > 0 || comment.changeTo && comment.changeTo.length > 0) { | ||
form.find('.comment-content, .to-value').removeClass('error'); | ||
hideNewCommentPopup(); | ||
callback(comment, index); | ||
} else { | ||
if (comment.text.length == 0) form.find('.comment-content').addClass('error'); | ||
if (comment.changeTo && comment.changeTo.length == 0) form.find('.to-value').addClass('error'); | ||
} | ||
@@ -64,103 +43,50 @@ return false; | ||
var fixFlyingToobarOnIOS = function() { | ||
if (browser.ios) { | ||
var shouldPlaceMenuRightOnBottom = $(".toolbar ul.menu_right").css('bottom') !== "auto"; | ||
getNewCommentContainer().find('input, textarea') | ||
.on("focus", function() { | ||
fixToolbarPosition(); | ||
if (shouldPlaceMenuRightOnBottom) placeMenuRightOnBottom(); | ||
}) | ||
.on("blur", function() { | ||
revertFixToToolbarPosition(); | ||
if (shouldPlaceMenuRightOnBottom) revertPlacingMenuRightOnBottom(); | ||
}); | ||
// When user changes orientation, we need to re-position menu_right | ||
if (shouldPlaceMenuRightOnBottom) { | ||
waitForResizeToFinishThenCall(500, function() { | ||
var needToUpdateTop = $(".toolbar ul.menu_right").css("top") !== ""; | ||
if (needToUpdateTop) placeMenuRightOnBottom(); | ||
}); | ||
} | ||
} | ||
} | ||
var fixToolbarPosition = function() { | ||
$(".toolbar ul.menu_left, .toolbar ul.menu_right").css("position", "absolute"); | ||
} | ||
var revertFixToToolbarPosition = function() { | ||
$(".toolbar ul.menu_left, .toolbar ul.menu_right").css("position", ""); | ||
} | ||
var placeMenuRightOnBottom = function() { | ||
$(".toolbar ul.menu_right").css("top", $(document).outerHeight()); | ||
} | ||
var revertPlacingMenuRightOnBottom = function() { | ||
$(".toolbar ul.menu_right").css("top", ""); | ||
} | ||
/* ***** Public methods: ***** */ | ||
var localizeNewCommentForm = function() { | ||
var newCommentForm = getNewCommentContainer().find('#newComment'); | ||
if (newCommentForm.length !== 0) commentL10n.localize(newCommentForm); | ||
var localizenewCommentPopup = function() { | ||
var newCommentPopup = $('#newComment'); | ||
if (newCommentPopup.length !== 0) commentL10n.localize(newCommentPopup); | ||
}; | ||
// Create container to hold new comment form | ||
var insertContainers = function(target) { | ||
target.prepend('<div id="newComments"></div>'); | ||
// Listen for include suggested change toggle | ||
getNewCommentContainer().on("change", '#suggestion-checkbox', function() { | ||
if($(this).is(':checked')) { | ||
getPadOuter().find('.suggestion').show(); | ||
} else { | ||
getPadOuter().find('.suggestion').hide(); | ||
} | ||
}); | ||
} | ||
// Insert new Comment Form | ||
var insertNewCommentFormIfDontExist = function(comment, callback) { | ||
var newCommentForm = getNewCommentContainer().find('#newComment'); | ||
var formDoesNotExist = newCommentForm.length === 0; | ||
if (formDoesNotExist) { | ||
newCommentForm = createNewCommentForm(comment); | ||
localizeNewCommentForm(); | ||
var insertNewCommentPopupIfDontExist = function(comment, callback) { | ||
$('#newComment').remove(); | ||
var newCommentPopup = $('#newComment'); | ||
// Listen to cancel | ||
newCommentForm.find('#comment-reset').on('click', function() { | ||
cancelNewComment(); | ||
}); | ||
comment.commentId = ""; | ||
var newCommentPopup = $('#newCommentTemplate').tmpl(comment); | ||
newCommentPopup.appendTo($('#editorcontainerbox')); | ||
// Hack to avoid "flying" toolbars on iOS | ||
fixFlyingToobarOnIOS(); | ||
localizenewCommentPopup(); | ||
} else { | ||
// Reset form to make sure it is all clear | ||
newCommentForm.get(0).reset(); | ||
// Listen for include suggested change toggle | ||
$('#newComment').find('.suggestion-checkbox').change(function() { | ||
$('#newComment').find('.suggestion').toggle($(this).is(':checked')); | ||
}); | ||
// Detach current "submit" handler to be able to call the updated callback | ||
newCommentForm.off("submit"); | ||
} | ||
// Listen to comment confirmation (needs to be outside of if/else to be able to update the callback) | ||
newCommentForm.submit(function() { | ||
var form = $(this); | ||
return submitNewComment(form, callback); | ||
// Cancel btn | ||
newCommentPopup.find('#comment-reset').on('click', function() { | ||
cancelNewComment(); | ||
}); | ||
// Create btn | ||
$('#newComment').on("submit", function(e) { | ||
e.preventDefault(); | ||
return submitNewComment(callback); | ||
}); | ||
return newCommentForm; | ||
return newCommentPopup; | ||
}; | ||
var showNewCommentForm = function() { | ||
getNewCommentContainer().addClass("active"); | ||
// we need to set a timeout otherwise the animation to show #newComment won't be visible | ||
window.setTimeout(function() { | ||
getPadOuter().find('.suggestion').hide(); // Hides suggestion in case of a cancel | ||
getNewCommentContainer().find('#newComment').removeClass("hidden").addClass("visible"); | ||
}, 0); | ||
var showNewCommentPopup = function() { | ||
// position below comment icon | ||
$('#newComment').css('left', $('.toolbar .addComment').offset().left) | ||
// Reset form to make sure it is all clear | ||
$('#newComment').find('.suggestion-checkbox').prop('checked', false).trigger('change'); | ||
$('#newComment').find('textarea').val(""); | ||
$('#newComment').find('.comment-content, .to-value').removeClass('error'); | ||
// Show popup | ||
$('#newComment').addClass("popup-show"); | ||
// mark selected text, so it is clear to user which text range the comment is being applied to | ||
@@ -170,13 +96,8 @@ pad.plugins.ep_comments_page.preCommentMarker.markSelectedText(); | ||
var hideNewCommentForm = function() { | ||
getNewCommentContainer().find('#newComment').removeClass("visible").addClass("hidden"); | ||
var hideNewCommentPopup = function() { | ||
$('#newComment').removeClass("popup-show"); | ||
// force focus to be lost, so virtual keyboard is hidden on mobile devices | ||
getNewCommentContainer().find(':focus').blur(); | ||
$('#newComment').find(':focus').blur(); | ||
// we need to give some time for the animation of #newComment to finish | ||
window.setTimeout(function() { | ||
getNewCommentContainer().removeClass("active"); | ||
}, 500); | ||
// unmark selected text, as now there is no text being commented | ||
@@ -186,19 +107,5 @@ pad.plugins.ep_comments_page.preCommentMarker.unmarkSelectedText(); | ||
// Some browsers trigger resize several times while resizing the window, so | ||
// we need to make sure resize is done to avoid calling the callback multiple | ||
// times. | ||
// Based on: https://css-tricks.com/snippets/jquery/done-resizing-event/ | ||
var waitForResizeToFinishThenCall = function(timeout, callback){ | ||
var resizeTimer; | ||
$(window).on("resize", function() { | ||
clearTimeout(resizeTimer); | ||
resizeTimer = setTimeout(callback, timeout); | ||
}); | ||
} | ||
exports.localizeNewCommentForm = localizeNewCommentForm; | ||
exports.insertNewCommentFormIfDontExist = insertNewCommentFormIfDontExist; | ||
exports.showNewCommentForm = showNewCommentForm; | ||
exports.hideNewCommentForm = hideNewCommentForm; | ||
exports.insertContainers = insertContainers; | ||
exports.waitForResizeToFinishThenCall = waitForResizeToFinishThenCall; | ||
exports.localizenewCommentPopup = localizenewCommentPopup; | ||
exports.insertNewCommentPopupIfDontExist = insertNewCommentPopupIfDontExist; | ||
exports.showNewCommentPopup = showNewCommentPopup; | ||
exports.hideNewCommentPopup = hideNewCommentPopup; |
@@ -0,8 +1,28 @@ | ||
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; | ||
var collectContentPre = function(hook, context){ | ||
var comment = /(?:^| )(c-[A-Za-z0-9]*)/.exec(context.cls); | ||
var fakeComment = /(?:^| )(fakecomment-[A-Za-z0-9]*)/.exec(context.cls); | ||
if(comment && comment[1]){ | ||
context.cc.doAttrib(context.state, "comment::" + comment[1]); | ||
} | ||
// a fake comment is a comment copied from this or another pad. To avoid conflicts | ||
// with existing comments, a fake commentId is used, so then we generate a new one | ||
// when the comment is saved | ||
if(fakeComment){ | ||
var mapFakeComments = pad.plugins.ep_comments_page.getMapfakeComments(); | ||
var fakeCommentId = fakeComment[1]; | ||
var commentId = mapFakeComments[fakeCommentId]; | ||
context.cc.doAttrib(context.state, "comment::" + commentId); | ||
} | ||
}; | ||
exports.collectContentPre = collectContentPre; | ||
exports.generateCommentId = function(){ | ||
var commentId = "c-" + randomString(16); | ||
return commentId; | ||
} |
@@ -22,3 +22,6 @@ var supertest = require('ep_etherpad-lite/node_modules/supertest'), | ||
beforeEach(function(done){ | ||
padID = createPad(done); | ||
createPad(function(err, newPadID) { | ||
padID = newPadID; | ||
done(err); | ||
}); | ||
}); | ||
@@ -49,4 +52,4 @@ | ||
// add a comment to a pad | ||
createComment(pad, {}, function(err, comment) { | ||
createCommentReply(pad, comment, {}, function(err, replyId){ | ||
createComment(padID, {}, function(err, comment) { | ||
createCommentReply(padID, comment, {}, function(err, replyId){ | ||
api.get(listCommentRepliesEndPointFor(padID, apiKey)) | ||
@@ -63,3 +66,3 @@ .expect(function(res){ | ||
it('returns comment replies data', function(done){ | ||
createComment(pad, {}, function(err, comment){ | ||
createComment(padID, {}, function(err, comment){ | ||
var text = "text"; | ||
@@ -78,3 +81,3 @@ var changeTo = "changeTo"; | ||
}; | ||
createCommentReply(pad, comment, data, function(err, replyId){ | ||
createCommentReply(padID, comment, data, function(err, replyId){ | ||
api.get(listCommentRepliesEndPointFor(padID, apiKey)) | ||
@@ -96,5 +99,5 @@ .expect(function(res){ | ||
// create comment | ||
createComment(pad, {}, function(err, comment){ | ||
createComment(padID, {}, function(err, comment){ | ||
// create reply | ||
createCommentReply(pad, comment, {}, function(err, commentId){ | ||
createCommentReply(padID, comment, {}, function(err, commentId){ | ||
// get r-w data | ||
@@ -105,3 +108,3 @@ api.get(listCommentRepliesEndPointFor(padID, apiKey)) | ||
// get read-only pad id | ||
readOnlyId(pad, function(err, roPadId) { | ||
readOnlyId(padID, function(err, roPadId) { | ||
// get r-o data | ||
@@ -127,5 +130,8 @@ api.get(listCommentRepliesEndPointFor(roPadId, apiKey)) | ||
// creates a new pad... | ||
padID = createPad(function(err, pad) { | ||
createPad(function(err, newPadID) { | ||
if (err) throw err; | ||
padID = newPadID; | ||
// ... and a comment before each test run | ||
createComment(pad, {}, function(err, comment) { | ||
createComment(padID, {}, function(err, comment) { | ||
commentID = comment; | ||
@@ -218,5 +224,8 @@ done(err); | ||
// creates a new pad... | ||
padID = createPad(function(err, pad) { | ||
createPad(function(err, newPadID) { | ||
if (err) throw err; | ||
padID = newPadID; | ||
// ... and a comment before each test run, then... | ||
createComment(pad, {},function(err, comment) { | ||
createComment(padID, {},function(err, comment) { | ||
commentID = comment; | ||
@@ -226,3 +235,3 @@ | ||
var socket = io.connect(appUrl + "/comment"); | ||
var req = { padId: pad }; | ||
var req = { padId: padID }; | ||
// needs to get comments to be able to join the pad room, where the messages will be broadcast to: | ||
@@ -263,2 +272,4 @@ socket.emit('getComments', req, function (res){ | ||
createPad(function(err, otherPadId) { | ||
if (err) throw err; | ||
// ... and another comment... | ||
@@ -265,0 +276,0 @@ createComment(otherPadId, {},function(err, otherCommentId) { |
@@ -20,3 +20,6 @@ var supertest = require('ep_etherpad-lite/node_modules/supertest'), | ||
beforeEach(function(done){ | ||
padID = createPad(done); | ||
createPad(function(err, newPadID) { | ||
padID = newPadID; | ||
done(err); | ||
}); | ||
}); | ||
@@ -47,5 +50,5 @@ | ||
// creates first comment... | ||
createComment(pad, {},function(err, comment) { | ||
createComment(padID, {},function(err, comment) { | ||
// ... creates second comment... | ||
createComment(pad, {},function(err, comment) { | ||
createComment(padID, {},function(err, comment) { | ||
// ... and finally checks if comments are returned | ||
@@ -76,3 +79,3 @@ api.get(listCommentsEndPointFor(padID, apiKey)) | ||
}; | ||
createComment(pad, data, function(err, commentId){ | ||
createComment(padID, data, function(err, commentId){ | ||
api.get(listCommentsEndPointFor(padID, apiKey)) | ||
@@ -92,7 +95,7 @@ .expect(function(res){ | ||
it('returns same data for read-write and read-only pad ids', function(done){ | ||
createComment(pad, {}, function(err, commentId){ | ||
createComment(padID, {}, function(err, commentId){ | ||
api.get(listCommentsEndPointFor(padID, apiKey)) | ||
.end(function(err, res) { | ||
var rwData = JSON.stringify(res.body.data); | ||
readOnlyId(pad, function(err, roPadId) { | ||
readOnlyId(padID, function(err, roPadId) { | ||
api.get(listCommentsEndPointFor(roPadId, apiKey)) | ||
@@ -115,3 +118,6 @@ .expect(function(res){ | ||
beforeEach(function(done){ | ||
padID = createPad(done); | ||
createPad(function(err, newPadID) { | ||
padID = newPadID; | ||
done(err); | ||
}); | ||
}); | ||
@@ -202,6 +208,9 @@ | ||
//create a new pad before each test run... | ||
padID = createPad(function(err, pad) { | ||
createPad(function(err, newPadID) { | ||
if (err) throw err; | ||
padID = newPadID; | ||
// ... and listens to the broadcast message: | ||
var socket = io.connect(appUrl + "/comment"); | ||
var req = { padId: pad }; | ||
var req = { padId: padID }; | ||
// needs to get comments to be able to join the pad room, where the messages will be broadcast to: | ||
@@ -220,3 +229,3 @@ socket.emit('getComments', req, function (res){ | ||
// create first comment... | ||
createComment(padID,{}, function(err, commentId) { | ||
createComment(padID, {}, function(err, commentId) { | ||
if(err) throw err; | ||
@@ -226,3 +235,3 @@ if(!commentId) throw new Error("Comment should had been created"); | ||
// ... create second comment... | ||
createComment(padID,{}, function(err, commentId) { | ||
createComment(padID, {}, function(err, commentId) { | ||
if(err) throw err; | ||
@@ -243,2 +252,4 @@ if(!commentId) throw new Error("Comment should had been created"); | ||
createPad(function(err, otherPadId) { | ||
if(err) throw err; | ||
// ... and add comment to it: | ||
@@ -245,0 +256,0 @@ createComment(otherPadId, {}, function(err, commentId) { |
@@ -1,2 +0,2 @@ | ||
describe("Comment Localization", function(){ | ||
describe("ep_comments_page - Comment Localization", function(){ | ||
//create a new pad with comment before each test run | ||
@@ -25,3 +25,3 @@ beforeEach(function(cb){ | ||
//get the title of the comment | ||
var $changeToLabel = outer$(".comment-changeTo-label").first(); | ||
var $changeToLabel = outer$(".to-label").first(); | ||
expect($changeToLabel.text()).to.be("Suggested Change:"); | ||
@@ -40,3 +40,3 @@ | ||
//get the 'Suggested Change' label | ||
var $changeToLabel = outer$("#" + commentId + " .comment-changeTo-label").first(); | ||
var $changeToLabel = outer$("#" + commentId + " .to-label").first(); | ||
expect($changeToLabel.text()).to.be("Alteração Sugerida:"); | ||
@@ -64,3 +64,3 @@ | ||
//get the 'Include suggested change' label | ||
var $changeToLabel = outer$('#newComment label[for=suggestion-checkbox]').first(); | ||
var $changeToLabel = outer$('#newComment label.label-suggestion-checkbox').first(); | ||
expect($changeToLabel.text()).to.be("Incluir alteração sugerida"); | ||
@@ -95,5 +95,5 @@ | ||
$commentField.val("My comment"); | ||
var $hasSuggestion = outer$("#suggestion-checkbox"); | ||
var $hasSuggestion = outer$(".suggestion-checkbox"); | ||
$hasSuggestion.click(); | ||
var $suggestionField = outer$("textarea.comment-suggest-to"); | ||
var $suggestionField = outer$("textarea.to-value"); | ||
$suggestionField.val("Change to this suggestion"); | ||
@@ -100,0 +100,0 @@ var $submittButton = outer$("input[type=submit]"); |
@@ -1,2 +0,2 @@ | ||
describe("Comment settings", function() { | ||
describe("ep_comments_page - Comment settings", function() { | ||
describe("when user unchecks 'Show Comments'", function() { | ||
@@ -12,2 +12,3 @@ // create a new pad and check "Show Comments" checkbox | ||
it("sidebar comments should not be visible when opening a new pad", function(done) { | ||
this.timeout(60000); | ||
// force to create a new pad, so validation would be on brand new pads | ||
@@ -22,2 +23,3 @@ helper.newPad(function() { | ||
it("sidebar comments should not be visible when adding a new comment to a new pad", function(done) { | ||
this.timeout(60000); | ||
// force to create a new pad, so validation would be on brand new pads | ||
@@ -85,5 +87,5 @@ helper.newPad(function() { | ||
$commentField.val("My comment"); | ||
var $hasSuggestion = outer$("#suggestion-checkbox"); | ||
var $hasSuggestion = outer$(".suggestion-checkbox"); | ||
$hasSuggestion.click(); | ||
var $suggestionField = outer$("textarea.comment-suggest-to"); | ||
var $suggestionField = outer$("textarea.to-value"); | ||
$suggestionField.val("Change to this suggestion"); | ||
@@ -109,2 +111,2 @@ var $submittButton = outer$("input[type=submit]"); | ||
} | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
describe("Comment Delete", function(){ | ||
describe("ep_comments_page - Comment Delete", function(){ | ||
//create a new pad with comment before each test run | ||
@@ -46,5 +46,5 @@ beforeEach(function(cb){ | ||
$commentField.val("My comment"); | ||
var $hasSuggestion = outer$("#suggestion-checkbox"); | ||
var $hasSuggestion = outer$(".suggestion-checkbox"); | ||
$hasSuggestion.click(); | ||
var $suggestionField = outer$("textarea.comment-suggest-to"); | ||
var $suggestionField = outer$("textarea.to-value"); | ||
$suggestionField.val("Change to this suggestion"); | ||
@@ -51,0 +51,0 @@ var $submittButton = outer$("input[type=submit]"); |
@@ -1,2 +0,2 @@ | ||
describe("Comment icons", function() { | ||
describe("ep_comments_page - Comment icons", function() { | ||
//create a new pad with comment before each test run | ||
@@ -249,5 +249,5 @@ beforeEach(function(cb){ | ||
// we don't need comment suggestion to be filled for these tests, but here's how to do it: | ||
// var $hasSuggestion = outer$("#suggestion-checkbox"); | ||
// var $hasSuggestion = outer$(".suggestion-checkbox"); | ||
// $hasSuggestion.click(); | ||
// var $suggestionField = outer$("textarea.comment-suggest-to"); | ||
// var $suggestionField = outer$("textarea.to-value"); | ||
// $suggestionField.val("Change to this suggestion"); | ||
@@ -254,0 +254,0 @@ var $submittButton = outer$("input[type=submit]"); |
@@ -1,2 +0,2 @@ | ||
describe("Comment Reply", function(){ | ||
describe("ep_comments_page - Comment Reply", function(){ | ||
//create a new pad with comment before each test run | ||
@@ -41,5 +41,5 @@ beforeEach(function(cb){ | ||
var $replyForm = outer$("form.comment-reply"); | ||
var $replyField = $replyForm.find(".comment-reply-input"); | ||
var $replyField = $replyForm.find(".comment-content"); | ||
var $replyWithSuggestionCheckbox = $replyForm.find(".reply-suggestion-checkbox"); | ||
var $replySuggestionTextarea = $replyForm.find(".reply-comment-suggest-to"); | ||
var $replySuggestionTextarea = $replyForm.find(".reply-to-value"); | ||
expect($replyField.text()).to.be(""); | ||
@@ -111,5 +111,5 @@ expect($replyWithSuggestionCheckbox.is(":checked")).to.be(false); | ||
$commentField.val("My comment"); | ||
var $hasSuggestion = outer$("#suggestion-checkbox"); | ||
var $hasSuggestion = outer$(".suggestion-checkbox"); | ||
$hasSuggestion.click(); | ||
var $suggestionField = outer$("textarea.comment-suggest-to"); | ||
var $suggestionField = outer$("textarea.to-value"); | ||
$suggestionField.val("Change to this suggestion"); | ||
@@ -139,3 +139,3 @@ var $submittButton = outer$("input[type=submit]"); | ||
// fill reply field | ||
var $replyField = outer$(".comment-reply-input"); | ||
var $replyField = outer$(".comment-content"); | ||
$replyField.val("My reply"); | ||
@@ -150,3 +150,3 @@ | ||
// fill suggestion field | ||
var $suggestionField = outer$("textarea.reply-comment-suggest-to"); | ||
var $suggestionField = outer$("textarea.reply-to-value"); | ||
$suggestionField.val("My suggestion"); | ||
@@ -153,0 +153,0 @@ } |
@@ -1,2 +0,2 @@ | ||
describe("Comment Suggestion", function(){ | ||
describe("ep_comments_page - Comment Suggestion", function(){ | ||
//create a new pad before each test run | ||
@@ -18,3 +18,3 @@ beforeEach(function(cb){ | ||
var $suggestionFrom = outer$(".comment-suggest-from"); | ||
var $suggestionFrom = outer$(".from-value"); | ||
expect($suggestionFrom.val()).to.be("A\n text with\n line attributes"); | ||
@@ -41,3 +41,3 @@ done(); | ||
var $suggestionFrom = outer$(".comment-suggest-from"); | ||
var $suggestionFrom = outer$(".from-value"); | ||
expect($suggestionFrom.val()).to.be('New target for comment'); | ||
@@ -69,5 +69,5 @@ done(); | ||
// check suggestion box | ||
var $hasSuggestion = outer$("#suggestion-checkbox"); | ||
var $hasSuggestion = outer$(".suggestion-checkbox"); | ||
$hasSuggestion.click(); | ||
} | ||
@@ -1,2 +0,2 @@ | ||
describe("Pre-comment text mark", function() { | ||
describe("ep_comments_page - Pre-comment text mark", function() { | ||
var padId; | ||
@@ -134,5 +134,5 @@ | ||
$commentField.val("My comment"); | ||
var $hasSuggestion = outer$("#suggestion-checkbox"); | ||
var $hasSuggestion = outer$(".suggestion-checkbox"); | ||
$hasSuggestion.click(); | ||
var $suggestionField = outer$("textarea.comment-suggest-to"); | ||
var $suggestionField = outer$("textarea.to-value"); | ||
$suggestionField.val("Change to this suggestion"); | ||
@@ -139,0 +139,0 @@ var $submittButton = outer$("input[type=submit]"); |
var prettyDate; | ||
describe("Time Formatting", function() { | ||
_.each({'en': 'English', 'de': 'a language not localized yet'}, function(description, lang) { | ||
describe("ep_comments_page - Time Formatting", function() { | ||
_.each({'en': 'English', 'af': 'a language not localized yet'}, function(description, lang) { | ||
describe("in " + description, function(){ | ||
@@ -390,3 +390,3 @@ before(function(cb) { | ||
'pt-br' : 'Negrito (Ctrl-B)', | ||
'de' : 'Fett (Strg-B)' | ||
'af' : 'Vet (Ctrl-B)' | ||
}; | ||
@@ -393,0 +393,0 @@ var chrome$ = helper.padChrome$; |
@@ -45,3 +45,3 @@ var appUrl = 'http://localhost:9001'; | ||
var createPad = function(done) { | ||
pad = randomString(5); | ||
var pad = randomString(5); | ||
@@ -54,4 +54,2 @@ api.get('/api/'+apiVersion+'/createPad?apikey='+apiKey+"&padID="+pad) | ||
done(null, pad); | ||
return pad; | ||
} | ||
@@ -58,0 +56,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
267050
50
6034
1