Stackedit/public/res/extensions/comments.js

344 lines
14 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
var comments = new Extension("comments", 'Comments');
2014-03-24 00:22:46 +00:00
var commentTmpl = [
'<div class="comment-block">',
' <div class="comment-author"><%= author %></div>',
' <div class="comment-content"><%= content %></div>',
'</div>',
].join('');
2014-03-25 00:23:42 +00:00
var popoverTitleTmpl = [
'<span class="clearfix">',
' <a href="#" class="action-remove-discussion pull-right">',
' <i class="icon-trash"></i>',
' </a>',
' <%- title %>',
'</span>',
].join('');
2014-03-24 00:22:46 +00:00
var eventMgr;
comments.onEventMgrCreated = function(eventMgrParam) {
eventMgr = eventMgrParam;
};
2014-03-22 01:57:31 +00:00
var offsetMap = {};
2014-03-23 02:33:41 +00:00
function setCommentEltCoordinates(commentElt, y) {
2014-03-24 00:22:46 +00:00
var lineIndex = Math.round(y / 10);
2014-03-23 02:33:41 +00:00
var top = (y - 8) + 'px';
var right = ((offsetMap[lineIndex] || 0) * 25 + 10) + 'px';
commentElt.style.top = top;
commentElt.style.right = right;
return lineIndex;
}
2014-03-22 01:57:31 +00:00
var inputElt;
var marginElt;
2014-03-23 02:33:41 +00:00
var commentEltList = [];
2014-03-22 01:57:31 +00:00
var newCommentElt = crel('a', {
class: 'icon-comment new'
});
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-23 02:33:41 +00:00
var refreshId;
2014-03-24 00:22:46 +00:00
var currentFileDesc;
2014-03-23 02:33:41 +00:00
function refreshDiscussions() {
2014-03-24 00:22:46 +00:00
if(currentFileDesc === undefined) {
return;
}
var author = storage['author.name'];
2014-03-23 02:33:41 +00:00
clearTimeout(refreshId);
commentEltList.forEach(function(commentElt) {
marginElt.removeChild(commentElt);
});
commentEltList = [];
offsetMap = {};
2014-03-24 00:22:46 +00:00
var discussionList = _.map(currentFileDesc.discussionList, _.identity);
2014-03-23 02:33:41 +00:00
function refreshOne() {
2014-03-25 00:23:42 +00:00
if(discussionList.length === 0) {
return;
}
var discussion = discussionList.pop();
2014-03-23 02:33:41 +00:00
var commentElt = crel('a', {
class: 'icon-comment'
});
commentElt.discussion = discussion;
var coordinates = inputElt.getOffsetCoordinates(discussion.selectionEnd);
var lineIndex = setCommentEltCoordinates(commentElt, coordinates.y);
offsetMap[lineIndex] = (offsetMap[lineIndex] || 0) + 1;
marginElt.appendChild(commentElt);
commentEltList.push(commentElt);
2014-03-24 00:22:46 +00:00
// Move newCommentElt
2014-03-23 02:33:41 +00:00
setCommentEltCoordinates(newCommentElt, cursorY);
2014-03-24 00:22:46 +00:00
// Apply class later for fade effect
commentElt.offsetWidth; // Refresh
var isReplied = _.last(discussion.commentList).author != author;
commentElt.className += isReplied ? ' replied' : ' added';
refreshId = setTimeout(refreshOne, 50);
2014-03-23 02:33:41 +00:00
}
refreshId = setTimeout(refreshOne, 50);
}
2014-03-24 00:22:46 +00:00
var debouncedRefreshDiscussions = _.debounce(refreshDiscussions, 2000);
comments.onFileOpen = function(fileDesc) {
currentFileDesc = fileDesc;
refreshDiscussions();
};
comments.onContentChanged = function(fileDesc, content) {
currentFileDesc === fileDesc && debouncedRefreshDiscussions();
};
var currentContext;
function closeCurrentPopover() {
currentContext && currentContext.$commentElt.popover('toggle').popover('destroy');
}
comments.onLayoutResize = function() {
closeCurrentPopover();
refreshDiscussions();
};
comments.onDiscussionCreated = function() {
refreshDiscussions();
};
comments.onDiscussionRemoved = function() {
refreshDiscussions();
};
comments.onCommentAdded = function() {
refreshDiscussions();
};
function getDiscussionComments() {
return currentContext.discussion.commentList.map(function(comment) {
return _.template(commentTmpl, {
author: comment.author || 'Anonymous',
content: comment.content
});
}).join('');
}
2014-03-23 02:33:41 +00:00
2014-03-22 01:57:31 +00:00
comments.onReady = function() {
var cssApplier = rangy.createCssClassApplier("comment-highlight", {
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-24 00:22:46 +00:00
var titleLength = currentContext.discussion.selectionEnd - currentContext.discussion.selectionStart;
var title = inputElt.textContent.substr(currentContext.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, {
title: title
});
2014-03-22 01:57:31 +00:00
},
content: function() {
var content = _.template(commentsPopoverContentHTML, {
2014-03-24 00:22:46 +00:00
commentList: getDiscussionComments()
2014-03-22 01:57:31 +00:00
});
return content;
},
selector: '#wmd-input > .editor-margin > .icon-comment'
}).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) {
2014-03-24 00:22:46 +00:00
closeCurrentPopover();
var context = {
2014-03-25 00:23:42 +00:00
$commentElt: $(evt.target).addClass('active'),
fileDesc: currentFileDesc
2014-03-24 00:22:46 +00:00
};
currentContext = context;
2014-03-23 02:33:41 +00:00
inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
// If it's an existing discussion
if(evt.target.discussion) {
2014-03-24 00:22:46 +00:00
context.discussion = evt.target.discussion;
context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd);
2014-03-23 02:33:41 +00:00
return;
}
2014-03-22 01:57:31 +00:00
// Get selected text
2014-03-24 00:22:46 +00:00
var selectionStart = inputElt.selectionStart;
var selectionEnd = inputElt.selectionEnd;
2014-03-22 01:57:31 +00:00
if(selectionStart === selectionEnd) {
var after = inputElt.textContent.substring(selectionStart);
var match = /\S+/.exec(after);
if(match) {
selectionStart += match.index;
if(match.index === 0) {
2014-03-23 02:33:41 +00:00
while(selectionStart && /\S/.test(inputElt.textContent[selectionStart - 1])) {
2014-03-22 01:57:31 +00:00
selectionStart--;
}
}
selectionEnd += match.index + match[0].length;
}
}
2014-03-24 00:22:46 +00:00
context.selectionRange = inputElt.createRange(selectionStart, selectionEnd);
context.discussion = {
2014-03-22 01:57:31 +00:00
selectionStart: selectionStart,
selectionEnd: selectionEnd,
2014-03-23 02:33:41 +00:00
commentList: []
2014-03-22 01:57:31 +00:00
};
}).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) {
// Move the popover in the margin
2014-03-24 00:22:46 +00:00
var context = currentContext;
context.popoverElt = document.querySelector('.comments-popover .popover:last-child');
var left = -5;
if(context.popoverElt.offsetWidth < marginElt.offsetWidth - 5) {
left = marginElt.offsetWidth - 10 - context.popoverElt.offsetWidth;
2014-03-22 01:57:31 +00:00
}
2014-03-24 00:22:46 +00:00
context.popoverElt.style.left = left + 'px';
context.popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(evt.target.style.right) - evt.target.offsetWidth / 2 - left) + 'px';
2014-03-22 01:57:31 +00:00
2014-03-24 00:22:46 +00:00
// Scroll to the bottom of the discussion
context.popoverElt.querySelector('.popover-content').scrollTop = 9999999;
context.$authorInputElt = $(context.popoverElt.querySelector('.input-comment-author')).val(storage['author.name']);
context.$contentInputElt = $(context.popoverElt.querySelector('.input-comment-content'));
var $addButton = $(context.popoverElt.querySelector('.action-add-comment'));
context.$contentInputElt.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-24 00:22:46 +00:00
context.$contentInputElt.val('');
closeCurrentPopover();
2014-03-25 00:23:42 +00:00
var discussionList = context.fileDesc.discussionList || {};
2014-03-24 00:22:46 +00:00
var isNew = false;
if(!context.discussion.discussionIndex) {
isNew = true;
2014-03-23 02:33:41 +00:00
// Create discussion index
var discussionIndex;
2014-03-22 01:57:31 +00:00
do {
2014-03-23 02:33:41 +00:00
discussionIndex = utils.randomString();
} while(_.has(discussionList, discussionIndex));
2014-03-24 00:22:46 +00:00
context.discussion.discussionIndex = discussionIndex;
discussionList[discussionIndex] = context.discussion;
2014-03-22 01:57:31 +00:00
}
2014-03-24 00:22:46 +00:00
context.discussion.commentList.push({
2014-03-22 01:57:31 +00:00
author: author,
content: content
});
2014-03-25 00:23:42 +00:00
context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
2014-03-24 00:22:46 +00:00
isNew ?
2014-03-25 00:23:42 +00:00
eventMgr.onDiscussionCreated(context.fileDesc, context.discussion) :
eventMgr.onCommentAdded(context.fileDesc, context.discussion);
2014-03-23 02:33:41 +00:00
inputElt.focus();
2014-03-22 01:57:31 +00:00
});
2014-03-24 00:22:46 +00:00
var $removeButton = $(context.popoverElt.querySelector('.action-remove-discussion'));
if(evt.target.discussion) {
// If it's an existing discussion
var $removeCancelButton = $(context.popoverElt.querySelector('.action-remove-discussion-cancel'));
var $removeConfirmButton = $(context.popoverElt.querySelector('.action-remove-discussion-confirm'));
$removeButton.click(function() {
$(context.popoverElt.querySelector('.new-comment-block')).addClass('hide');
$(context.popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide');
context.popoverElt.querySelector('.popover-content').scrollTop = 9999999;
});
$removeCancelButton.click(function() {
$(context.popoverElt.querySelector('.new-comment-block')).removeClass('hide');
$(context.popoverElt.querySelector('.remove-discussion-confirm')).addClass('hide');
context.popoverElt.querySelector('.popover-content').scrollTop = 9999999;
context.$contentInputElt.focus();
});
$removeConfirmButton.click(function() {
closeCurrentPopover();
2014-03-25 00:23:42 +00:00
delete context.fileDesc.discussionList[context.discussion.discussionIndex];
context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage
eventMgr.onDiscussionRemoved(context.fileDesc, context.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-24 00:22:46 +00:00
$(context.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-22 01:57:31 +00:00
}).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) {
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-22 01:57:31 +00:00
});
};
return comments;
});