Stackedit/public/res/extensions/comments.js

469 lines
19 KiB
JavaScript
Raw Normal View History

2014-03-22 01:57:31 +00:00
define([
"jquery",
"underscore",
"utils",
2014-03-24 00:22:46 +00:00
"storage",
2014-03-22 01:57:31 +00:00
"crel",
"rangy",
"classes/Extension",
"text!html/commentsPopoverContent.html",
"bootstrap"
2014-03-24 00:22:46 +00:00
], function($, _, utils, storage, crel, rangy, Extension, commentsPopoverContentHTML) {
2014-03-22 01:57:31 +00:00
2014-04-06 00:59:32 +00:00
var comments = new Extension("comments", 'Comments', false, true);
2014-03-22 01:57:31 +00:00
2014-03-24 00:22:46 +00:00
var commentTmpl = [
2014-03-31 00:10:28 +00:00
'<div class="comment-block<%= reply ? \' reply\' : \'\' %>">',
' <div class="comment-author"><i class="icon-comment"></i> <%= author %></div>',
2014-03-24 00:22:46 +00:00
' <div class="comment-content"><%= content %></div>',
'</div>',
].join('');
2014-03-25 00:23:42 +00:00
var popoverTitleTmpl = [
'<span class="clearfix">',
2014-04-02 23:35:07 +00:00
' <a href="#" class="action-remove-discussion pull-right">',
2014-03-25 00:23:42 +00:00
' <i class="icon-trash"></i>',
' </a>',
2014-03-31 00:10:28 +00:00
' “<%- title %>”',
2014-03-25 00:23:42 +00:00
'</span>',
].join('');
2014-03-24 00:22:46 +00:00
var eventMgr;
comments.onEventMgrCreated = function(eventMgrParam) {
eventMgr = eventMgrParam;
};
2014-04-05 00:54:06 +00:00
var editor;
2014-04-06 23:39:24 +00:00
var selectionMgr;
2014-04-05 00:54:06 +00:00
comments.onEditorCreated = function(editorParam) {
editor = editorParam;
2014-04-06 23:39:24 +00:00
selectionMgr = editor.selectionMgr;
2014-04-05 00:54:06 +00:00
};
2014-03-22 01:57:31 +00:00
var offsetMap = {};
2014-03-23 02:33:41 +00:00
function setCommentEltCoordinates(commentElt, y) {
2014-04-06 23:39:24 +00:00
var lineIndex = Math.round(y / 5);
// Try to find the nearest existing lineIndex
if(offsetMap.hasOwnProperty(lineIndex)) {
// Keep current lineIndex
}
else if(offsetMap.hasOwnProperty(lineIndex - 1)) {
lineIndex--;
}
else if(offsetMap.hasOwnProperty(lineIndex + 1)) {
lineIndex++;
}
var yOffset = -8;
2014-04-02 23:35:07 +00:00
if(commentElt.className.indexOf(' icon-split') !== -1) {
2014-03-30 01:44:51 +00:00
yOffset = -12;
}
2014-04-06 00:59:32 +00:00
var top = y + yOffset;
var right = (offsetMap[lineIndex] || 0) * 25 + 10;
commentElt.orderedIndex = lineIndex * 10000 + right;
commentElt.style.top = top + 'px';
commentElt.style.right = right + 'px';
2014-03-23 02:33:41 +00:00
return lineIndex;
}
2014-03-22 01:57:31 +00:00
var inputElt;
var marginElt;
var newCommentElt = crel('a', {
2014-04-06 23:39:24 +00:00
class: 'discussion icon-comment new'
2014-03-22 01:57:31 +00:00
});
2014-03-23 02:33:41 +00:00
var cursorY;
comments.onCursorCoordinates = function(x, y) {
cursorY = y;
setCommentEltCoordinates(newCommentElt, cursorY);
2014-03-22 01:57:31 +00:00
};
2014-03-31 00:10:28 +00:00
function Context(commentElt, fileDesc) {
this.commentElt = commentElt;
this.$commentElt = $(commentElt).addClass('active');
this.fileDesc = fileDesc;
this.discussionIndex = commentElt.discussionIndex;
}
Context.prototype.getDiscussion = function() {
if(!this.discussionIndex) {
return this.fileDesc.newDiscussion;
}
return this.fileDesc.discussionList[this.discussionIndex];
};
Context.prototype.getPopoverElt = function() {
return document.querySelector('.comments-popover .popover:last-child');
};
2014-03-27 00:20:08 +00:00
var currentContext;
function movePopover(commentElt) {
// Move popover in the margin
2014-03-31 00:10:28 +00:00
var popoverElt = currentContext.getPopoverElt();
2014-03-27 00:20:08 +00:00
var left = 0;
2014-03-31 00:10:28 +00:00
if(popoverElt.offsetWidth < marginElt.offsetWidth - 10) {
left = marginElt.offsetWidth - 10 - popoverElt.offsetWidth;
2014-03-27 00:20:08 +00:00
}
2014-03-31 00:10:28 +00:00
popoverElt.style.left = left + 'px';
popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(commentElt.style.right) - commentElt.offsetWidth / 2 - left) + 'px';
2014-04-14 00:21:06 +00:00
var popoverTopOffset = window.innerHeight - currentContext.hr.getBoundingClientRect().top;
2014-04-06 00:59:32 +00:00
if(popoverTopOffset < 0) {
popoverElt.style.top = (parseInt(popoverElt.style.top) + popoverTopOffset) + 'px';
}
2014-03-27 00:20:08 +00:00
}
2014-03-26 00:29:34 +00:00
var cssApplier;
2014-03-24 00:22:46 +00:00
var currentFileDesc;
2014-03-30 01:44:51 +00:00
var refreshTimeoutId;
2014-03-31 00:10:28 +00:00
var commentEltMap = {};
2014-04-06 00:59:32 +00:00
var sortedCommentEltList = [];
var someReplies = false;
var $openDiscussionElt;
var $openDiscussionIconElt;
2014-03-27 00:20:08 +00:00
var refreshDiscussions = _.debounce(function() {
2014-03-24 00:22:46 +00:00
if(currentFileDesc === undefined) {
return;
}
2014-04-06 00:59:32 +00:00
someReplies = false;
sortedCommentEltList = [];
2014-03-24 00:22:46 +00:00
var author = storage['author.name'];
2014-03-23 02:33:41 +00:00
offsetMap = {};
2014-03-30 01:44:51 +00:00
var discussionList = _.values(currentFileDesc.discussionList);
function refreshOne() {
if(discussionList.length === 0) {
2014-03-31 00:10:28 +00:00
// Remove outdated commentElt
_.filter(commentEltMap, function(commentElt, discussionIndex) {
return !_.has(currentFileDesc.discussionList, discussionIndex);
}).forEach(function(commentElt) {
marginElt.removeChild(commentElt);
2014-04-02 23:35:07 +00:00
delete commentEltMap[commentElt.discussionIndex];
2014-03-31 00:10:28 +00:00
});
2014-03-30 01:44:51 +00:00
// Move newCommentElt
setCommentEltCoordinates(newCommentElt, cursorY);
2014-03-31 00:10:28 +00:00
if(currentContext && !currentContext.discussionIndex) {
2014-03-30 01:44:51 +00:00
inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
movePopover(newCommentElt);
}
2014-04-06 00:59:32 +00:00
sortedCommentEltList = _.sortBy(commentEltMap, function(commentElt) {
return commentElt.orderedIndex;
});
$openDiscussionElt.toggleClass('some', sortedCommentEltList.length !== 0);
$openDiscussionElt.toggleClass('replied', someReplies);
$openDiscussionIconElt.toggleClass('icon-chat', sortedCommentEltList.length !== 0);
2014-03-30 01:44:51 +00:00
return;
}
var discussion = discussionList.pop();
2014-04-06 00:59:32 +00:00
var commentElt = commentEltMap[discussion.discussionIndex];
if(!commentElt) {
commentElt = crel('a');
}
var className = 'discussion';
2014-04-02 23:35:07 +00:00
var isReplied = !discussion.commentList || _.last(discussion.commentList).author != author;
2014-04-06 00:59:32 +00:00
isReplied && (someReplies = true);
2014-03-30 01:44:51 +00:00
if(discussion.type == 'conflict') {
2014-04-06 00:59:32 +00:00
className += ' icon-split';
2014-03-30 01:44:51 +00:00
}
2014-04-06 23:39:24 +00:00
else {
className += ' icon-comment';
}
className += isReplied ? ' replied' : ' added';
2014-04-06 00:59:32 +00:00
commentElt.className = className;
2014-03-27 00:20:08 +00:00
commentElt.discussionIndex = discussion.discussionIndex;
2014-04-06 23:39:24 +00:00
var coordinates = selectionMgr.getCoordinates(discussion.selectionEnd);
2014-03-23 02:33:41 +00:00
var lineIndex = setCommentEltCoordinates(commentElt, coordinates.y);
offsetMap[lineIndex] = (offsetMap[lineIndex] || 0) + 1;
2014-03-31 00:10:28 +00:00
2014-03-23 02:33:41 +00:00
marginElt.appendChild(commentElt);
2014-03-31 00:10:28 +00:00
commentEltMap[discussion.discussionIndex] = commentElt;
2014-03-23 02:33:41 +00:00
2014-04-08 23:20:48 +00:00
if(currentContext && currentContext.getDiscussion() === discussion) {
2014-03-27 00:20:08 +00:00
inputElt.scrollTop += parseInt(commentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
movePopover(commentElt);
}
2014-03-30 01:44:51 +00:00
refreshTimeoutId = setTimeout(refreshOne, 5);
2014-03-23 02:33:41 +00:00
}
2014-03-30 01:44:51 +00:00
clearTimeout(refreshTimeoutId);
refreshTimeoutId = setTimeout(refreshOne, 5);
2014-03-27 00:20:08 +00:00
}, 50);
2014-04-08 23:20:48 +00:00
comments.onLayoutResize = refreshDiscussions;
2014-03-24 00:22:46 +00:00
comments.onFileOpen = function(fileDesc) {
currentFileDesc = fileDesc;
refreshDiscussions();
};
2014-03-28 00:49:49 +00:00
comments.onContentChanged = function(fileDesc) {
2014-03-27 00:20:08 +00:00
currentFileDesc === fileDesc && refreshDiscussions();
2014-03-24 00:22:46 +00:00
};
2014-03-26 00:29:34 +00:00
comments.onCommentsChanged = function(fileDesc) {
2014-03-27 00:20:08 +00:00
if(currentFileDesc !== fileDesc) {
return;
}
if(currentContext !== undefined) {
// Refresh conversation if popover is open
var context = currentContext;
2014-03-31 00:10:28 +00:00
if(context.discussionIndex) {
2014-04-12 22:30:18 +00:00
context.getPopoverElt().querySelector('.discussion-comment-list').innerHTML = getDiscussionComments();
2014-03-27 00:20:08 +00:00
}
try {
cssApplier.undoToRange(context.rangyRange);
}
catch(e) {}
2014-03-31 00:10:28 +00:00
var discussion = context.getDiscussion();
2014-04-06 23:39:24 +00:00
context.selectionRange = selectionMgr.createRange(discussion.selectionStart, discussion.selectionEnd);
2014-03-27 00:20:08 +00:00
// Highlight selected text
context.rangyRange = rangy.createRange();
context.rangyRange.setStart(context.selectionRange.startContainer, context.selectionRange.startOffset);
context.rangyRange.setEnd(context.selectionRange.endContainer, context.selectionRange.endOffset);
setTimeout(function() { // Need to delay this because it's not refreshed properly
if(currentContext === context) {
cssApplier.applyToRange(context.rangyRange);
}
}, 50);
}
refreshDiscussions();
2014-03-26 00:29:34 +00:00
};
2014-03-24 00:22:46 +00:00
function closeCurrentPopover() {
currentContext && currentContext.$commentElt.popover('toggle').popover('destroy');
}
2014-03-27 00:20:08 +00:00
comments.onDiscussionCreated = function(fileDesc) {
currentFileDesc === fileDesc && refreshDiscussions();
};
comments.onDiscussionRemoved = function(fileDesc, discussion) {
if(currentFileDesc === fileDesc) {
2014-03-30 01:44:51 +00:00
// Close popover if the discussion has been removed
2014-03-31 00:10:28 +00:00
if(currentContext !== undefined && currentContext.discussionIndex == discussion.discussionIndex) {
2014-03-27 00:20:08 +00:00
closeCurrentPopover();
}
refreshDiscussions();
}
};
2014-03-24 00:22:46 +00:00
function getDiscussionComments() {
2014-03-31 00:10:28 +00:00
var discussion = currentContext.getDiscussion();
var author = storage['author.name'];
2014-04-02 23:35:07 +00:00
var result = [];
if(discussion.commentList) {
result = discussion.commentList.map(function(comment) {
var commentAuthor = comment.author || 'Anonymous';
return _.template(commentTmpl, {
author: commentAuthor,
content: comment.content,
reply: comment.author != author
});
});
}
2014-03-31 00:10:28 +00:00
if(discussion.type == 'conflict') {
2014-04-02 23:35:07 +00:00
result.unshift(_.template(commentTmpl, {
author: 'StackEdit',
2014-04-06 00:59:32 +00:00
content: 'Multiple users have made conflicting modifications.',
2014-04-02 23:35:07 +00:00
reply: true
}));
2014-03-30 01:44:51 +00:00
}
2014-04-02 23:35:07 +00:00
return result.join('');
2014-03-24 00:22:46 +00:00
}
2014-03-23 02:33:41 +00:00
2014-03-22 01:57:31 +00:00
comments.onReady = function() {
2014-03-26 00:29:34 +00:00
cssApplier = rangy.createCssClassApplier("comment-highlight", {
2014-03-22 01:57:31 +00:00
normalize: false
});
2014-03-24 00:22:46 +00:00
var previousContent = '';
2014-03-22 01:57:31 +00:00
inputElt = document.getElementById('wmd-input');
marginElt = document.querySelector('#wmd-input > .editor-margin');
marginElt.appendChild(newCommentElt);
2014-03-23 02:33:41 +00:00
$(document.body).append(crel('div', {
class: 'comments-popover'
2014-03-24 00:22:46 +00:00
})).on('click', function(evt) {
// Close on click outside the popover
if(currentContext && currentContext.$commentElt[0] !== evt.target) {
closeCurrentPopover();
}
}).popover({
2014-03-22 01:57:31 +00:00
placement: 'auto top',
container: '.comments-popover',
html: true,
title: function() {
2014-03-24 00:22:46 +00:00
if(!currentContext) {
return true;
2014-03-22 01:57:31 +00:00
}
2014-03-31 00:10:28 +00:00
var discussion = currentContext.getDiscussion();
var titleLength = discussion.selectionEnd - discussion.selectionStart;
2014-04-05 00:54:06 +00:00
var title = editor.getValue().substr(discussion.selectionStart, titleLength > 20 ? 20 : titleLength);
2014-03-23 02:33:41 +00:00
if(titleLength > 20) {
2014-03-22 01:57:31 +00:00
title += '...';
}
2014-03-25 00:23:42 +00:00
return _.template(popoverTitleTmpl, {
2014-03-30 01:44:51 +00:00
title: title,
2014-03-25 00:23:42 +00:00
});
2014-03-22 01:57:31 +00:00
},
content: function() {
var content = _.template(commentsPopoverContentHTML, {
2014-03-30 01:44:51 +00:00
commentList: getDiscussionComments(),
2014-03-22 01:57:31 +00:00
});
return content;
},
2014-03-30 01:44:51 +00:00
selector: '#wmd-input > .editor-margin > .discussion'
2014-03-22 01:57:31 +00:00
}).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) {
2014-03-24 00:22:46 +00:00
closeCurrentPopover();
2014-03-31 00:10:28 +00:00
var context = new Context(evt.target, currentFileDesc);
2014-03-24 00:22:46 +00:00
currentContext = context;
2014-03-23 02:33:41 +00:00
2014-04-08 23:20:48 +00:00
// If it's not an existing discussion
2014-03-31 00:10:28 +00:00
var discussion = context.getDiscussion();
2014-04-08 23:20:48 +00:00
if(!discussion) {
// Get selected text
var selectionStart = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
var selectionEnd = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
if(selectionStart === selectionEnd) {
var offset = selectionMgr.getClosestWordOffset(selectionStart);
selectionStart = offset.start;
selectionEnd = offset.end;
}
discussion = {
selectionStart: selectionStart,
selectionEnd: selectionEnd,
commentList: []
};
currentFileDesc.newDiscussion = discussion;
2014-03-23 02:33:41 +00:00
}
2014-04-08 23:20:48 +00:00
context.selectionRange = selectionMgr.setSelectionStartEnd(discussion.selectionStart, discussion.selectionEnd, undefined, true);
inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
2014-03-22 01:57:31 +00:00
}).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) {
2014-03-24 00:22:46 +00:00
var context = currentContext;
2014-03-31 00:10:28 +00:00
var popoverElt = context.getPopoverElt();
2014-04-06 00:59:32 +00:00
context.$authorInputElt = $(popoverElt.querySelector('.input-comment-author')).val(storage['author.name']);
context.$contentInputElt = $(popoverElt.querySelector('.input-comment-content'));
2014-04-06 23:39:24 +00:00
context.hr = popoverElt.querySelector('hr');
2014-04-06 00:59:32 +00:00
movePopover(context.commentElt);
2014-03-22 01:57:31 +00:00
2014-03-24 00:22:46 +00:00
// Scroll to the bottom of the discussion
2014-04-06 00:59:32 +00:00
popoverElt.querySelector('.scrollport').scrollTop = 9999999;
2014-03-24 00:22:46 +00:00
2014-03-31 00:10:28 +00:00
var $addButton = $(popoverElt.querySelector('.action-add-comment'));
$().add(context.$contentInputElt).add(context.$authorInputElt).keydown(function(evt) {
2014-03-23 02:33:41 +00:00
// Enter key
switch(evt.which) {
case 13:
evt.preventDefault();
$addButton.click();
return;
case 27:
evt.preventDefault();
2014-03-24 00:22:46 +00:00
closeCurrentPopover();
2014-03-23 02:33:41 +00:00
inputElt.focus();
return;
}
});
$addButton.click(function(evt) {
2014-03-24 00:22:46 +00:00
var author = utils.getInputTextValue(context.$authorInputElt);
var content = utils.getInputTextValue(context.$contentInputElt, evt);
2014-03-22 01:57:31 +00:00
if(evt.isPropagationStopped()) {
return;
}
2014-03-31 00:10:28 +00:00
var discussion = context.getDiscussion();
2014-03-24 00:22:46 +00:00
context.$contentInputElt.val('');
closeCurrentPopover();
2014-04-02 23:35:07 +00:00
discussion.commentList = discussion.commentList || [];
2014-03-31 00:10:28 +00:00
discussion.commentList.push({
2014-03-27 00:20:08 +00:00
author: author,
content: content
});
2014-03-25 00:23:42 +00:00
var discussionList = context.fileDesc.discussionList || {};
2014-03-31 00:10:28 +00:00
if(!discussion.discussionIndex) {
2014-03-23 02:33:41 +00:00
// Create discussion index
var discussionIndex;
2014-03-22 01:57:31 +00:00
do {
discussionIndex = utils.randomString();
2014-03-23 02:33:41 +00:00
} while(_.has(discussionList, discussionIndex));
2014-03-31 00:10:28 +00:00
discussion.discussionIndex = discussionIndex;
discussionList[discussionIndex] = discussion;
2014-03-27 00:20:08 +00:00
context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
2014-03-31 00:10:28 +00:00
eventMgr.onDiscussionCreated(context.fileDesc, discussion);
2014-03-27 00:20:08 +00:00
}
else {
context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
eventMgr.onCommentsChanged(context.fileDesc);
2014-03-22 01:57:31 +00:00
}
2014-03-23 02:33:41 +00:00
inputElt.focus();
2014-03-22 01:57:31 +00:00
});
2014-03-31 00:10:28 +00:00
var $removeButton = $(popoverElt.querySelector('.action-remove-discussion'));
2014-03-27 00:20:08 +00:00
if(evt.target.discussionIndex) {
2014-03-24 00:22:46 +00:00
// If it's an existing discussion
$removeButton.click(function() {
closeCurrentPopover();
2014-03-31 00:10:28 +00:00
var discussion = context.getDiscussion();
delete context.fileDesc.discussionList[discussion.discussionIndex];
2014-03-25 00:23:42 +00:00
context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage
2014-03-31 00:10:28 +00:00
eventMgr.onDiscussionRemoved(context.fileDesc, discussion);
2014-03-24 00:22:46 +00:00
inputElt.focus();
});
}
else {
// Otherwise hide the remove button
$removeButton.hide();
}
2014-03-22 01:57:31 +00:00
// Prevent from closing on click inside the popover
2014-03-31 00:10:28 +00:00
$(popoverElt).on('click', function(evt) {
2014-03-22 01:57:31 +00:00
evt.stopPropagation();
});
2014-03-24 00:22:46 +00:00
// Highlight selected text
context.rangyRange = rangy.createRange();
context.rangyRange.setStart(context.selectionRange.startContainer, context.selectionRange.startOffset);
context.rangyRange.setEnd(context.selectionRange.endContainer, context.selectionRange.endOffset);
setTimeout(function() { // Need to delay this because it's not refreshed properly
if(currentContext === context) {
cssApplier.applyToRange(context.rangyRange);
}
}, 50);
// Focus on textarea
context.$contentInputElt.focus().val(previousContent);
2014-03-28 00:49:49 +00:00
}).on('hide.bs.popover', '#wmd-input > .editor-margin', function() {
2014-03-24 00:22:46 +00:00
if(!currentContext) {
return;
}
currentContext.$commentElt.removeClass('active');
// Save content and author for later
previousContent = currentContext.$contentInputElt.val();
storage['author.name'] = currentContext.$authorInputElt.val();
2014-03-22 01:57:31 +00:00
// Remove highlight
2014-03-24 00:22:46 +00:00
cssApplier.undoToRange(currentContext.rangyRange);
currentContext = undefined;
2014-03-26 00:29:34 +00:00
delete currentFileDesc.newDiscussion;
2014-03-22 01:57:31 +00:00
});
2014-04-06 00:59:32 +00:00
var $newCommentElt = $(newCommentElt);
$openDiscussionElt = $('.button-open-discussion').click(function(evt) {
var $commentElt = $newCommentElt;
2014-04-06 23:39:24 +00:00
if(currentContext) {
if(!currentContext.discussionIndex) {
$commentElt = $(_.first(sortedCommentEltList) || newCommentElt);
2014-04-06 00:59:32 +00:00
}
else {
var curentIndex = -1;
sortedCommentEltList.some(function(elt, index) {
if(elt === currentContext.commentElt) {
curentIndex = index;
return true;
}
});
$commentElt = $(sortedCommentEltList[(curentIndex + 1) % sortedCommentEltList.length]);
}
}
$commentElt.click();
evt.stopPropagation();
});
$openDiscussionIconElt = $openDiscussionElt.find('i');
2014-03-22 01:57:31 +00:00
};
return comments;
});