diff --git a/index.js b/index.js index 24269675..91e4892e 100644 --- a/index.js +++ b/index.js @@ -111,6 +111,7 @@ exports.eejsBlock_editbarMenuLeft = function (hook_name, args, cb) { exports.eejsBlock_scripts = function (hook_name, args, cb) { args.content = args.content + eejs.require("ep_comments_page/templates/comments.html", {}, module); + args.content = args.content + eejs.require("ep_comments_page/templates/commentIcons.html", {}, module); return cb(); }; diff --git a/static/css/commentIcon.css b/static/css/commentIcon.css new file mode 100644 index 00000000..e514fa98 --- /dev/null +++ b/static/css/commentIcon.css @@ -0,0 +1,34 @@ +#commentIcons { + position: relative; + display: block; + z-index: 1; + left: 760px; +} + +.comment-icon-line { + position: absolute; +} +.comment-icon { + background-repeat: no-repeat; + display: inline-block; + height: 16px; + vertical-align: middle; + width: 16px; + margin-right: 5px; +} +.comment-icon:before { + font-family: "fontawesome-etherpad"; + content: "\e829"; + color:#666; + font-size:14px; + padding-top:2px; + line-height:17px; +} + +.comment-icon.with-reply:before { + content: "\e828"; +} + +.comment-icon.active:before { + color:orange; +} diff --git a/static/js/commentBoxes.js b/static/js/commentBoxes.js new file mode 100644 index 00000000..dd072177 --- /dev/null +++ b/static/js/commentBoxes.js @@ -0,0 +1,75 @@ +// Easier access to outter pad +var padOuter; +var getPadOuter = function() { + padOuter = padOuter || $('iframe[name="ace_outer"]').contents(); + return padOuter; +} + +var getCommentsContainer = function() { + return getPadOuter().find("#comments"); +} + +/* ***** Public methods: ***** */ + +var showComment = function(commentId, e) { + var commentElm = getCommentsContainer().find('#'+ commentId); + commentElm.show(); + + highlightComment(commentId, e); +}; + +var hideComment = function(commentId, hideCommentTitle) { + var commentElm = getCommentsContainer().find('#'+ commentId); + commentElm.removeClass('mouseover'); + + // hide even the comment title + if (hideCommentTitle) commentElm.hide(); + + getPadOuter().contents().find('.comment-modal').hide(); +}; + +var hideOpenedComments = function() { + var openedComments = getCommentsContainer().find('.mouseover'); + openedComments.removeClass('mouseover').hide(); + + getPadOuter().contents().find('.comment-modal').hide(); +} + +var hideAllComments = function() { + getCommentsContainer().children().hide(); +} + +var highlightComment = function(commentId, e){ + var container = getCommentsContainer(); + var commentElm = container.find('#'+ commentId); + var commentsVisible = container.is(":visible"); + if(commentsVisible) { + // sidebar view highlight + commentElm.addClass('mouseover'); + } else { + var commentElm = container.find('#'+ commentId); + getPadOuter().contents().find('.comment-modal').show().css({ + left: e.clientX +"px", + top: e.clientY + 25 +"px" + }); + // hovering comment view + getPadOuter().contents().find('.comment-modal-comment').html(commentElm.html()); + } +} + +// Adjust position of the comment detail on the container, to be on the same +// height of the pad text associated to the comment, and return the affected element +var adjustTopOf = function(commentId, baseTop) { + var commentElement = getPadOuter().find('#'+commentId); + var targetTop = baseTop - 5; + commentElement.css("top", targetTop+"px"); + + return commentElement; +} + +exports.showComment = showComment; +exports.hideComment = hideComment; +exports.hideOpenedComments = hideOpenedComments; +exports.hideAllComments = hideAllComments; +exports.highlightComment = highlightComment; +exports.adjustTopOf = adjustTopOf; diff --git a/static/js/commentIcons.js b/static/js/commentIcons.js new file mode 100644 index 00000000..67d08225 --- /dev/null +++ b/static/js/commentIcons.js @@ -0,0 +1,178 @@ +var $ = require('ep_etherpad-lite/static/js/rjquery').$; +var _ = require('ep_etherpad-lite/static/js/underscore'); +var commentBoxes = require('ep_comments_page/static/js/commentBoxes'); + +// Easier access to outer pad +var padOuter; +var getPadOuter = function() { + padOuter = padOuter || $('iframe[name="ace_outer"]').contents(); + return padOuter; +} + +// easier access to inner pad +var padInner; +var getPadInner = function() { + padInner = padInner || getPadOuter().find('iframe[name="ace_inner"]'); + return padInner; +} + +var getOrCreateIconsContainerAt = function(top) { + var iconContainer = getPadOuter().find('#commentIcons'); + var iconClass = "icon-at-"+top; + + // is this the 1st comment on that line? + var iconsAtLine = iconContainer.find("."+iconClass); + var isFirstIconAtLine = iconsAtLine.length === 0; + + // create container for icons at target line, if it does not exist yet + if (isFirstIconAtLine) { + iconContainer.append('
'); + iconsAtLine = iconContainer.find("."+iconClass); + iconsAtLine.css("top", top+"px"); + } + + return iconsAtLine; +} + +var targetCommentIdOf = function(e) { + return e.currentTarget.getAttribute("data-commentid"); +} + +var highlightTargetTextOf = function(commentId) { + getPadInner().contents().find("head").append(""); +} + +var removeHighlightOfTargetTextOf = function(commentId) { + getPadInner().contents().find("head").append(""); + // TODO this could potentially break ep_font_color +} + +var toggleActiveCommentIcon = function(target) { + target.toggleClass("active").toggleClass("inactive"); +} + +var addListeners = function() { + getPadOuter().find('#commentIcons').on("mouseover", ".comment-icon", function(e){ + var commentId = targetCommentIdOf(e); + highlightTargetTextOf(commentId); + }).on("mouseout", ".comment-icon", function(e){ + var commentId = targetCommentIdOf(e); + removeHighlightOfTargetTextOf(commentId); + }).on("click", ".comment-icon.active", function(e){ + toggleActiveCommentIcon($(this)); + + var commentId = targetCommentIdOf(e); + commentBoxes.hideComment(commentId, true); + }).on("click", ".comment-icon.inactive", function(e){ + // deactivate/hide other comment boxes that are opened, so we have only + // one comment box opened at a time + commentBoxes.hideOpenedComments(); + var allActiveIcons = getPadOuter().find('#commentIcons').find(".comment-icon.active"); + toggleActiveCommentIcon(allActiveIcons); + + // activate/show only target comment + toggleActiveCommentIcon($(this)); + var commentId = targetCommentIdOf(e); + commentBoxes.showComment(commentId, e); + }); +} + +/* ***** Public methods: ***** */ + +// Create container to hold comment icons +var insertContainer = function() { + // we're only doing something if icons will be displayed at all + if (!clientVars.displayCommentAsIcon) return; + + getPadInner().before('
'); + + addListeners(); +} + +// Create a new comment icon +var addIcon = function(commentId, comment){ + // we're only doing something if icons will be displayed at all + if (!clientVars.displayCommentAsIcon) return; + + var inlineComment = getPadInner().contents().find(".comment."+commentId); + var top = inlineComment.get(0).offsetTop + 5; + var iconsAtLine = getOrCreateIconsContainerAt(top); + var icon = $('#commentIconTemplate').tmpl(comment); + + icon.appendTo(iconsAtLine); +} + +// Hide comment icons from container +var hideIcons = function() { + // we're only doing something if icons will be displayed at all + if (!clientVars.displayCommentAsIcon) return; + + getPadOuter().find('#commentIcons').children().children().each(function(){ + $(this).hide(); + }); +} + +// Adjust position of the comment icon on the container, to be on the same +// height of the pad text associated to the comment, and return the affected icon +var adjustTopOf = function(commentId, baseTop) { + // we're only doing something if icons will be displayed at all + if (!clientVars.displayCommentAsIcon) return; + + var icon = getPadOuter().find('#icon-'+commentId); + var targetTop = baseTop+5; + var iconsAtLine = getOrCreateIconsContainerAt(targetTop); + + // move icon from one line to the other + if (iconsAtLine != icon.parent()) icon.appendTo(iconsAtLine); + + icon.show(); + + return icon; +} + +// Indicate if comment detail currently opened was shown by a click on +// comment icon. +var isCommentOpenedByClickOnIcon = function() { + // we're only doing something if icons will be displayed at all + if (!clientVars.displayCommentAsIcon) return false; + + var iconClicked = getPadOuter().find('#commentIcons').find(".comment-icon.active"); + var commentOpenedByClickOnIcon = iconClicked.length !== 0; + + return commentOpenedByClickOnIcon; +} + +// Mark comment as a comment-with-reply, so it can be displayed with a +// different icon +var commentHasReply = function(commentId) { + // we're only doing something if icons will be displayed at all + if (!clientVars.displayCommentAsIcon) return; + + // change comment icon + var iconForComment = getPadOuter().find('#commentIcons').find("#icon-"+commentId); + iconForComment.addClass("with-reply"); +} + +// Indicate if comment should be shown, checking if it had the characteristics +// of a comment that was being displayed on the screen +var shouldShow = function(commentElement) { + var shouldShowComment = false; + + if (!clientVars.displayCommentAsIcon) { + // if icons are not being displayed, we always show comments + shouldShowComment = true; + } else if (commentElement.hasClass("mouseover")) { + // if icons are being displayed, we only show comments clicked by user + shouldShowComment = true; + } + + return shouldShowComment; +} + +exports.insertContainer = insertContainer; +exports.addIcon = addIcon; +exports.hideIcons = hideIcons; +exports.adjustTopOf = adjustTopOf; +exports.isCommentOpenedByClickOnIcon = isCommentOpenedByClickOnIcon; +exports.commentHasReply = commentHasReply; +exports.shouldShow = shouldShow; diff --git a/static/js/index.js b/static/js/index.js index e037dba4..f09b17d3 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -4,8 +4,10 @@ var $ = require('ep_etherpad-lite/static/js/rjquery').$; var _ = require('ep_etherpad-lite/static/js/underscore'); var padcookie = require('ep_etherpad-lite/static/js/pad_cookie').padcookie; var prettyDate = require('ep_comments_page/static/js/timeFormat').prettyDate; +var commentBoxes = require('ep_comments_page/static/js/commentBoxes'); +var commentIcons = require('ep_comments_page/static/js/commentIcons'); -var cssFiles = ['ep_comments_page/static/css/comment.css']; +var cssFiles = ['ep_comments_page/static/css/comment.css', 'ep_comments_page/static/css/commentIcon.css']; /************************************************************************/ /* ep_comments Plugin */ @@ -47,6 +49,9 @@ ep_comments.prototype.init = function(){ this.findContainers(); this.insertContainers(); + // Init icons container + commentIcons.insertContainer(); + // Get all comments this.getComments(function (comments){ if (!$.isEmptyObject(comments)){ @@ -338,22 +343,28 @@ ep_comments.prototype.collectComments = function(callback){ }); // hover event - this.padInner.contents().on("mouseover", ".comment" ,function(e){ - self.highlightComment(e); + this.padInner.contents().on("mouseover", ".comment", function(e){ + var commentId = self.commentIdOf(e); + commentBoxes.highlightComment(commentId, e); }); // click event - this.padInner.contents().on("click", ".comment" ,function(e){ - self.highlightComment(e); + this.padInner.contents().on("click", ".comment", function(e){ + var commentId = self.commentIdOf(e); + commentBoxes.highlightComment(commentId, e); }); - this.padInner.contents().on("mouseleave", ".comment" ,function(e){ - var cls = e.currentTarget.classList; - var classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); - var commentId = (classCommentId) ? classCommentId[1] : null; - var commentElm = $('iframe[name="ace_outer"]').contents().find("#comments").find('#'+ commentId); - commentElm.removeClass('mouseover'); - $('iframe[name="ace_outer"]').contents().find('.comment-modal').hide(); + this.padInner.contents().on("mouseleave", ".comment", function(e){ + var commentOpenedByClickOnIcon = commentIcons.isCommentOpenedByClickOnIcon(); + + // only hides comment if it was not opened by a click on the icon + if (!commentOpenedByClickOnIcon) { + var cls = e.currentTarget.classList; + var classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); + var commentId = (classCommentId) ? classCommentId[1] : null; + + commentBoxes.hideComment(commentId); + } }); self.setYofComments(); }; @@ -367,6 +378,10 @@ ep_comments.prototype.collectCommentReplies = function(callback){ var padComment = this.padInner.contents().find('.comment'); $.each(this.commentReplies, function(replyId, replies){ var commentId = replies.commentId; + + // tell comment icon that this comment has 1+ replies + commentIcons.commentHasReply(commentId); + var existsAlready = $('iframe[name="ace_outer"]').contents().find('#'+replyId).length; if(existsAlready) return; @@ -376,25 +391,12 @@ ep_comments.prototype.collectCommentReplies = function(callback){ }); }; -ep_comments.prototype.highlightComment = function(e){ +ep_comments.prototype.commentIdOf = function(e){ var cls = e.currentTarget.classList; var classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); - var commentId = (classCommentId) ? classCommentId[1] : null; - var commentElm = $('iframe[name="ace_outer"]').contents().find("#comments").find('#'+ commentId); - var commentsVisible = this.container.is(":visible"); - if(commentsVisible){ - // sidebar view highlight - commentElm.addClass('mouseover'); - }else{ - var commentElm = $('iframe[name="ace_outer"]').contents().find("#comments").find('#'+ commentId); - $('iframe[name="ace_outer"]').contents().find('.comment-modal').show().css({ - left: e.clientX +"px", - top: e.clientY + 25 +"px" - }); - // hovering comment view - $('iframe[name="ace_outer"]').contents().find('.comment-modal-comment').html(commentElm.html()); - } -} + + return (classCommentId) ? classCommentId[1] : null; +}; // Insert comment container in sidebar ep_comments.prototype.insertContainers = function(){ @@ -478,29 +480,44 @@ ep_comments.prototype.insertComment = function(commentId, comment, index, isNew) } else { commentAfterIndex.before(content); } + + // insert icon + if (!isNew) commentIcons.addIcon(commentId, comment); + this.setYofComments(); }; -// Set all comments ot be inline with their target REP +// Set all comments to be inline with their target REP ep_comments.prototype.setYofComments = function(){ // for each comment in the pad var padOuter = $('iframe[name="ace_outer"]').contents(); var padInner = padOuter.find('iframe[name="ace_inner"]'); var inlineComments = padInner.contents().find(".comment"); - padOuter.find("#comments").children("div").each(function(){ - // hide each outer comment - $(this).hide(); - }); + var commentsToBeShown = []; + + // hide each outer comment... + commentBoxes.hideAllComments(); + // ... and hide comment icons too + commentIcons.hideIcons(); + $.each(inlineComments, function(){ var y = this.offsetTop; - y = y-5; - var commentId = /(?:^| )c-([A-Za-z0-9]*)/.exec(this.className); // classname is the ID of the comment - if(commentId){ - var commentEle = padOuter.find('#c-'+commentId[1]) // find the comment - commentEle.css("top", y+"px").show(); + var commentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(this.className); // classname is the ID of the comment + if(commentId) { + // adjust outer comment... + var commentEle = commentBoxes.adjustTopOf(commentId[1], y); + // ... and adjust icons too + commentIcons.adjustTopOf(commentId[1], y); + + // mark this comment to be displayed if it was visible before we start adjusting its position + if (commentIcons.shouldShow(commentEle)) commentsToBeShown.push(commentEle); } }); + // re-display comments that were visible before + _.each(commentsToBeShown, function(commentEle) { + commentEle.show(); + }); }; ep_comments.prototype.localize = function(element) { @@ -760,27 +777,15 @@ var hooks = { }, aceEditEvent: function(hook, context){ - var padOuter = $('iframe[name="ace_outer"]').contents(); + // var padOuter = $('iframe[name="ace_outer"]').contents(); // padOuter.find('#sidediv').removeClass("sidedivhidden"); // TEMPORARY to do removing authorship colors can add sidedivhidden class to sidesiv! if(!context.callstack.docTextChanged) return; - // for each comment - // NOTE this is duplicate code because of the way this is written, ugh, this needs fixing - var padInner = padOuter.find('iframe[name="ace_inner"]'); - var inlineComments = padInner.contents().find(".comment"); - padOuter.find("#comments").children().each(function(){ - // hide each outer comment - $(this).hide(); - }); - $.each(inlineComments, function(){ - var y = this.offsetTop; - var commentId = /(?:^| )c-([A-Za-z0-9]*)/.exec(this.className); - if(commentId){ - var commentEle = padOuter.find('#c-'+commentId[1]); - y = y-5; - commentEle.css("top", y+"px").show(); - } - }); + // 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(); + } }, // Insert comments classes diff --git a/templates/commentIcons.html b/templates/commentIcons.html new file mode 100644 index 00000000..280c2d19 --- /dev/null +++ b/templates/commentIcons.html @@ -0,0 +1,4 @@ +