Index: lams_build/conf/etherpad/etherpad-lite/node_modules/ep_comments_page/static/js/index.js
===================================================================
diff -u
--- lams_build/conf/etherpad/etherpad-lite/node_modules/ep_comments_page/static/js/index.js (revision 0)
+++ lams_build/conf/etherpad/etherpad-lite/node_modules/ep_comments_page/static/js/index.js (revision 369986ca1f01a82723cf49bd1f07d417dd17e356)
@@ -0,0 +1,1325 @@
+'use strict';
+
+/* TODO:
+- lable reply textarea
+- Make the chekbox appear above the suggested changes even when activated
+*/
+
+
+const _ = require('ep_etherpad-lite/static/js/underscore');
+const browser = require('ep_etherpad-lite/static/js/browser');
+const commentBoxes = require('ep_comments_page/static/js/commentBoxes');
+const commentIcons = require('ep_comments_page/static/js/commentIcons');
+const commentL10n = require('ep_comments_page/static/js/commentL10n');
+const events = require('ep_comments_page/static/js/copyPasteEvents');
+const moment = require('ep_comments_page/static/js/moment-with-locales.min');
+const newComment = require('ep_comments_page/static/js/newComment');
+const padcookie = require('ep_etherpad-lite/static/js/pad_cookie').padcookie;
+const preCommentMark = require('ep_comments_page/static/js/preCommentMark');
+
+const getCommentIdOnFirstPositionSelected = events.getCommentIdOnFirstPositionSelected;
+const hasCommentOnSelection = events.hasCommentOnSelection;
+
+const cssFiles = [
+ 'ep_comments_page/static/css/comment.css',
+ 'ep_comments_page/static/css/commentIcon.css',
+];
+
+const UPDATE_COMMENT_LINE_POSITION_EVENT = 'updateCommentLinePosition';
+
+/* ********************************************************************
+ * ep_comments Plugin *
+ ******************************************************************** */
+
+// Container
+const EpComments = function (context) {
+ this.container = null;
+ this.padOuter = null;
+ this.padInner = null;
+ this.ace = context.ace;
+
+ // Required for instances running on weird ports
+ // This probably needs some work for instances running on root or not on /p/
+ const loc = document.location;
+ const port = loc.port === '' ? (loc.protocol === 'https:' ? 443 : 80) : loc.port;
+ const url = `${loc.protocol}//${loc.hostname}:${port}/comment`;
+ this.socket = io.connect(url);
+
+ this.padId = clientVars.padId;
+ this.comments = [];
+ this.commentReplies = {};
+ this.mapFakeComments = [];
+ this.mapOriginalCommentsId = [];
+ this.shouldCollectComment = false;
+ this.init();
+ this.preCommentMarker = preCommentMark.init(this.ace);
+};
+
+// Init Etherpad plugin comment pads
+EpComments.prototype.init = function () {
+ const self = this;
+ moment.locale(html10n.getLanguage());
+
+ // Init prerequisite
+ this.findContainers();
+ this.insertContainers(); // Insert comment containers in sidebar
+
+ // Init icons container
+ commentIcons.insertContainer();
+
+ // Get all comments
+ this.getComments((comments) => {
+ if (!$.isEmptyObject(comments)) {
+ this.setComments(comments);
+ this.collectComments();
+ }
+ });
+
+ this.getCommentReplies((replies) => {
+ if (!$.isEmptyObject(replies)) {
+ this.commentReplies = replies;
+ this.collectCommentReplies();
+ }
+ this.commentRepliesListen();
+ this.commentListen();
+ });
+
+ // Init add push event
+ this.pushComment('add', (commentId, comment) => {
+ this.setComment(commentId, comment);
+ this.collectCommentsAfterSomeIntervalsOfTime();
+ });
+
+ // When language is changed, we need to reload the comments to make sure
+ // all templates are localized
+ html10n.bind('localized', () => {
+ // Fall back to 'en' if moment.js doesn't support the language.
+ moment.locale([html10n.getLanguage(), 'en']);
+ this.localizeExistingComments();
+ });
+
+ // Recalculate position when editor is resized
+ $('#settings input, #skin-variant-full-width').on('change', (e) => {
+ this.setYofComments();
+ });
+ this.padInner.contents().on(UPDATE_COMMENT_LINE_POSITION_EVENT, (e) => {
+ this.setYofComments();
+ });
+ $(window).resize(_.debounce(() => { this.setYofComments(); }, 100));
+
+ // On click comment icon toolbar
+ $('.addComment').on('click', (e) => {
+ e.preventDefault(); // stops focus from being lost
+ this.displayNewCommentForm();
+ });
+
+ // 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.parent().on('click', '.comment-delete', async function () {
+ const commentId = $(this).closest('.comment-container')[0].id;
+ try {
+ await new Promise((resolve, reject) => {
+ self.socket.emit('deleteComment', {
+ padId: self.padId,
+ commentId,
+ authorId: clientVars.userId,
+ }, (errMsg) => errMsg ? reject(new Error(errMsg)) : resolve());
+ });
+ } catch (err) {
+ if (err.message !== 'unauth') throw err; // Let the uncaught error handler handle it.
+ $.gritter.add({
+ title: html10n.translations['ep_comments_page.error'] || 'Error',
+ text: html10n.translations['ep_comments_page.error.delete_unauth'] ||
+ 'You cannot delete other users comments!',
+ class_name: 'error',
+ });
+ return;
+ }
+ self.deleteComment(commentId);
+ const padOuter = $('iframe[name="ace_outer"]').contents();
+ const padInner = padOuter.find('iframe[name="ace_inner"]');
+ const selector = `.${commentId}`;
+ const ace = self.ace;
+
+ ace.callWithAce((aceTop) => {
+ const 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, (index, rep) => {
+ // I don't think we need this nested call
+ ace.callWithAce((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);
+ });
+
+ // 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 () {
+ const $commentBox = $(this).closest('.comment-container');
+ $commentBox.addClass('editing');
+
+ const 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
+ const data = {};
+ data.text = textBox.text();
+ const content = $('#editCommentTemplate').tmpl(data);
+ // localize the comment/reply edit form
+ commentL10n.localize(content);
+ // insert form
+ textBox.before(content);
+ }
+ });
+
+ // submit the edition on the text and update the comment text
+ this.container.parent().on('click', '.comment-edit-submit', async function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ const $commentBox = $(this).closest('.comment-container');
+ const $commentForm = $(this).closest('.comment-edit-form');
+ const commentId = $commentBox.data('commentid');
+ const commentText = $commentForm.find('.comment-edit-text').val();
+ const data = {};
+ data.commentId = commentId;
+ data.padId = clientVars.padId;
+ data.commentText = commentText;
+ data.authorId = clientVars.userId;
+
+ try {
+ await new Promise((resolve, reject) => {
+ self.socket.emit('updateCommentText', data,
+ (errMsg) => errMsg ? reject(new Error(errMsg)) : resolve());
+ });
+ } catch (err) {
+ if (err.message !== 'unauth') throw err; // Let the uncaught error handler handle it.
+ $.gritter.add({
+ title: html10n.translations['ep_comments_page.error'] || 'Error',
+ text: html10n.translations['ep_comments_page.error.edit_unauth'] ||
+ 'You cannot edit other users comments!',
+ class_name: 'error',
+ });
+ return;
+ }
+ $commentForm.remove();
+ $commentBox.removeClass('editing');
+ self.updateCommentBoxText(commentId, commentText);
+
+ // 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);
+ });
+
+ // 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();
+ const $commentBox = $(this).closest('.comment-container');
+ const textBox = self.findCommentText($commentBox).last();
+ textBox.siblings('.comment-edit-form').remove();
+ $commentBox.removeClass('editing');
+ });
+
+ // Listen for include suggested change toggle
+ this.container.parent().on('change', '.suggestion-checkbox', function () {
+ const parentComment = $(this).closest('.comment-container');
+ const parentSuggest = $(this).closest('.comment-reply');
+
+ if ($(this).is(':checked')) {
+ const commentId = parentComment.data('commentid');
+ const padOuter = $('iframe[name="ace_outer"]').contents();
+ const padInner = padOuter.find('iframe[name="ace_inner"]');
+
+ const currentString = padInner.contents().find(`.${commentId}`).html();
+
+ parentSuggest.find('.from-value').html(currentString);
+ parentSuggest.find('.suggestion').show();
+ } else {
+ parentSuggest.find('.suggestion').hide();
+ }
+ });
+
+ // User accepts or revert a change
+ this.container.parent().on('submit', '.comment-changeTo-form', function (e) {
+ e.preventDefault();
+ const data = self.getCommentData();
+ const commentEl = $(this).closest('.comment-container');
+ data.commentId = commentEl.data('commentid');
+ const padOuter = $('iframe[name="ace_outer"]').contents();
+ const padInner = padOuter.find('iframe[name="ace_inner"]').contents();
+
+ // Are we reverting a change?
+ const isRevert = commentEl.hasClass('change-accepted');
+ let 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)
+ const parentId = $(this).closest('.sidebar-comment').data('commentid');
+ // Nuke all that aren't first lines of this comment
+ padInner.find(`.${parentId}:not(:first)`).html('');
+
+ const padCommentSpan = padInner.find(`.${parentId}`).first();
+ newString = newString.replace(/(?:\r\n|\r)/g, '
');
+
+ // Write the new pad contents
+ padCommentSpan.html(newString);
+
+ if (isRevert) {
+ // Tell all users this change was reverted
+ self.socket.emit('revertChange', data, () => {});
+ self.showChangeAsReverted(data.commentId);
+ } else {
+ // Tell all users this change was accepted
+ self.socket.emit('acceptChange', data, () => {});
+ // 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
+ });
+
+ // 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();
+
+ const data = self.getCommentData();
+ 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, () => {
+ self.getCommentReplies((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');
+ });
+ });
+
+ $(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
+ // LAMS modifies this logic so right comments bar is hidden by default
+ $('#options-comments').prop('checked', padcookie.getPref('comments') === true);
+
+ $('#options-comments').on('change', () => {
+ const checked = $('#options-comments').is(':checked');
+ padcookie.setPref('comments', checked);
+ this.padOuter.find('#comments, #commentIcons').toggleClass('active', checked);
+ $('body').toggleClass('comments-active', checked);
+ $('iframe[name="ace_outer"]').contents().find('body').toggleClass('comments-active', checked);
+ });
+
+ // 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:
+ //