Comments support

This commit is contained in:
benweet 2014-03-24 00:22:46 +00:00
parent e310d234cf
commit 8b565e32ce
8 changed files with 374 additions and 207 deletions

View File

@ -1,7 +1,7 @@
define([], function() { define([], function() {
var constants = {}; var constants = {};
constants.VERSION = "3.1.9"; constants.VERSION = "3.1.9";
constants.MAIN_URL = "https://stackedit.io/"; constants.MAIN_URL = "https://stackedit.io/";
constants.GOOGLE_ANALYTICS_ACCOUNT_ID = "UA-39556145-1"; constants.GOOGLE_ANALYTICS_ACCOUNT_ID = "UA-39556145-1";
constants.GOOGLE_API_KEY = "AIzaSyAeCU8CGcSkn0z9js6iocHuPBX4f_mMWkw"; constants.GOOGLE_API_KEY = "AIzaSyAeCU8CGcSkn0z9js6iocHuPBX4f_mMWkw";
@ -14,7 +14,7 @@ define([], function() {
constants.DEFAULT_FILE_TITLE = "Title"; constants.DEFAULT_FILE_TITLE = "Title";
constants.DEFAULT_FOLDER_NAME = "New folder"; constants.DEFAULT_FOLDER_NAME = "New folder";
constants.GDRIVE_DEFAULT_FILE_TITLE = "New Markdown document"; constants.GDRIVE_DEFAULT_FILE_TITLE = "New Markdown document";
constants.EDITOR_DEFAULT_PADDING = 30; constants.EDITOR_DEFAULT_PADDING = 35;
constants.CHECK_ONLINE_PERIOD = 120000; constants.CHECK_ONLINE_PERIOD = 120000;
constants.AJAX_TIMEOUT = 30000; constants.AJAX_TIMEOUT = 30000;
constants.ASYNC_TASK_DEFAULT_TIMEOUT = 60000; constants.ASYNC_TASK_DEFAULT_TIMEOUT = 60000;
@ -28,7 +28,7 @@ define([], function() {
constants.PICASA_PROXY_URL = "https://stackedit-picasa-proxy.herokuapp.com/"; constants.PICASA_PROXY_URL = "https://stackedit-picasa-proxy.herokuapp.com/";
constants.SSH_PROXY_URL = "https://stackedit-ssh-proxy.herokuapp.com/"; constants.SSH_PROXY_URL = "https://stackedit-ssh-proxy.herokuapp.com/";
constants.HTMLTOPDF_URL = "https://stackedit-htmltopdf.herokuapp.com/"; constants.HTMLTOPDF_URL = "https://stackedit-htmltopdf.herokuapp.com/";
// Site dependent // Site dependent
constants.BASE_URL = "http://localhost/"; constants.BASE_URL = "http://localhost/";
constants.GOOGLE_CLIENT_ID = '241271498917-lev37kef013q85avc91am1gccg5g8lrb.apps.googleusercontent.com'; constants.GOOGLE_CLIENT_ID = '241271498917-lev37kef013q85avc91am1gccg5g8lrb.apps.googleusercontent.com';
@ -37,7 +37,7 @@ define([], function() {
constants.TUMBLR_PROXY_URL = "https://stackedit-tumblr-proxy-local.herokuapp.com/"; constants.TUMBLR_PROXY_URL = "https://stackedit-tumblr-proxy-local.herokuapp.com/";
constants.WORDPRESS_CLIENT_ID = '23361'; constants.WORDPRESS_CLIENT_ID = '23361';
constants.WORDPRESS_PROXY_URL = "https://stackedit-io-wordpress-proxy.herokuapp.com/"; constants.WORDPRESS_PROXY_URL = "https://stackedit-io-wordpress-proxy.herokuapp.com/";
if(location.hostname.indexOf("stackedit.io") === 0) { if(location.hostname.indexOf("stackedit.io") === 0) {
constants.BASE_URL = constants.MAIN_URL; constants.BASE_URL = constants.MAIN_URL;
constants.GOOGLE_CLIENT_ID = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com'; constants.GOOGLE_CLIENT_ID = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com';
@ -61,7 +61,7 @@ define([], function() {
constants.GATEKEEPER_URL = "https://stackedit-gatekeeper-insomnia.herokuapp.com/"; constants.GATEKEEPER_URL = "https://stackedit-gatekeeper-insomnia.herokuapp.com/";
constants.TUMBLR_PROXY_URL = "https://stackedit-tumblr-proxy-beta.herokuapp.com/"; constants.TUMBLR_PROXY_URL = "https://stackedit-tumblr-proxy-beta.herokuapp.com/";
} }
constants.THEME_LIST = { constants.THEME_LIST = {
"default": "Default", "default": "Default",
"gray": "Gray", "gray": "Gray",
@ -69,6 +69,6 @@ define([], function() {
"night": "Night", "night": "Night",
"school": "School", "school": "School",
}; };
return constants; return constants;
}); });

View File

@ -397,7 +397,7 @@ define([
if(pagedownEditor !== undefined) { if(pagedownEditor !== undefined) {
// If the editor is already created // If the editor is already created
$editorElt.val(initDocumentContent); editor.contentElt.textContent = initDocumentContent;
pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop); pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
$editorElt.focus(); $editorElt.focus();
return; return;
@ -455,7 +455,7 @@ define([
eventMgr.onPagedownConfigure(pagedownEditor); eventMgr.onPagedownConfigure(pagedownEditor);
pagedownEditor.hooks.chain("onPreviewRefresh", eventMgr.onAsyncPreview); pagedownEditor.hooks.chain("onPreviewRefresh", eventMgr.onAsyncPreview);
pagedownEditor.run(); pagedownEditor.run();
$editorElt.val(initDocumentContent); editor.contentElt.textContent = initDocumentContent;
pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop); pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
$editorElt.focus(); $editorElt.focus();
@ -799,7 +799,7 @@ define([
}); });
$(".action-import-docs-settings-confirm").click(function() { $(".action-import-docs-settings-confirm").click(function() {
storage.clear(); storage.clear();
var allowedKeys = /^file\.|^folder\.|^publish\.|^settings$|^sync\.|^google\.|^themeV3$|^version$/; var allowedKeys = /^file\.|^folder\.|^publish\.|^settings$|^sync\.|^google\.|^author\.|^themeV3$|^version$/;
_.each(newstorage, function(value, key) { _.each(newstorage, function(value, key) {
if(allowedKeys.test(key)) { if(allowedKeys.test(key)) {
storage[key] = value; storage[key] = value;

View File

@ -61,23 +61,66 @@ define([
fileDesc = selectedFileDesc; fileDesc = selectedFileDesc;
}); });
function saveEditorState() { function saveSelectionState() {
if(!inputElt.focused) { var selection = window.getSelection();
return; if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
var element = range.startContainer;
if ((inputElt.compareDocumentPosition(element) & 0x10)) {
var container = element;
var offset = range.startOffset;
do {
while (element = element.previousSibling) {
if (element.textContent) {
offset += element.textContent.length;
}
}
element = container = container.parentNode;
} while (element && element != inputElt);
selectionStart = offset;
selectionEnd = offset + (range + '').length;
}
} }
selectionStart = inputElt.selectionStart;
selectionEnd = inputElt.selectionEnd;
scrollTop = inputElt.scrollTop;
if(fileChanged === false) { if(fileChanged === false) {
fileDesc.editorStart = selectionStart; fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd; fileDesc.editorEnd = selectionEnd;
fileDesc.editorScrollTop = scrollTop;
} }
} }
var previousTextContent; var previousTextContent;
function getContentChange(textContent) {
// Find the first modified char
var startIndex = 0;
var startIndexMax = Math.min(previousTextContent.length, textContent.length);
while (startIndex < startIndexMax) {
if (previousTextContent.charCodeAt(startIndex) !== textContent.charCodeAt(startIndex)) {
break;
}
startIndex++;
}
// Find the last modified char
var endIndex = 1;
var endIndexMax = Math.min(previousTextContent.length - startIndex, textContent.length - startIndex);
while (endIndex <= endIndexMax) {
if (previousTextContent.charCodeAt(previousTextContent.length - endIndex) !== textContent.charCodeAt(textContent.length - endIndex)) {
break;
}
endIndex++;
}
var replacement = textContent.substring(startIndex, textContent.length - endIndex + 1);
endIndex = previousTextContent.length - endIndex + 1;
return {
startIndex: startIndex,
endIndex: endIndex,
replacement: replacement
};
}
function checkContentChange() { function checkContentChange() {
saveEditorState(); saveSelectionState();
var currentTextContent = inputElt.textContent; var currentTextContent = inputElt.textContent;
if(fileChanged === false) { if(fileChanged === false) {
if(currentTextContent == previousTextContent) { if(currentTextContent == previousTextContent) {
@ -86,6 +129,37 @@ define([
if(!/\n$/.test(currentTextContent)) { if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n'; currentTextContent += '\n';
} }
var change = getContentChange(currentTextContent);
var endOffset = change.startIndex + change.replacement.length - change.endIndex;
// Move comments according to change
var updateDiscussionList = false;
_.each(fileDesc.discussionList, function(discussion) {
if(discussion.isRemoved === true) {
return;
}
// selectionEnd
if(discussion.selectionEnd >= change.endIndex) {
discussion.selectionEnd += endOffset;
updateDiscussionList = true;
}
else if(discussion.selectionEnd > change.startIndex) {
discussion.selectionEnd = change.startIndex;
updateDiscussionList = true;
}
// selectionStart
if(discussion.selectionStart >= change.endIndex) {
discussion.selectionStart += endOffset;
updateDiscussionList = true;
}
else if(discussion.selectionStart > change.startIndex) {
discussion.selectionStart = change.startIndex;
updateDiscussionList = true;
}
});
if(updateDiscussionList === true) {
fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage
}
fileDesc.content = currentTextContent; fileDesc.content = currentTextContent;
eventMgr.onContentChanged(fileDesc, currentTextContent); eventMgr.onContentChanged(fileDesc, currentTextContent);
} }
@ -199,7 +273,7 @@ define([
var cursorY = 0; var cursorY = 0;
var isBackwardSelection = false; var isBackwardSelection = false;
function updateCursorCoordinates() { function updateCursorCoordinates() {
saveEditorState(); saveSelectionState();
$inputElt.toggleClass('has-selection', selectionStart !== selectionEnd); $inputElt.toggleClass('has-selection', selectionStart !== selectionEnd);
var element; var element;
@ -234,24 +308,25 @@ define([
eventMgr.onCursorCoordinates(coordinates.x, coordinates.y); eventMgr.onCursorCoordinates(coordinates.x, coordinates.y);
} }
function adjustCursorPosition() { var adjustCursorPosition = _.debounce(function() {
inputElt && setTimeout(function() { if(inputElt === undefined) {
updateCursorCoordinates(); return;
}
updateCursorCoordinates();
var adjust = inputElt.offsetHeight / 2; var adjust = inputElt.offsetHeight / 2;
if(adjust > 130) { if(adjust > 130) {
adjust = 130; adjust = 130;
} }
var cursorMinY = inputElt.scrollTop + adjust; var cursorMinY = inputElt.scrollTop + adjust;
var cursorMaxY = inputElt.scrollTop + inputElt.offsetHeight - adjust; var cursorMaxY = inputElt.scrollTop + inputElt.offsetHeight - adjust;
if(cursorY < cursorMinY) { if(cursorY < cursorMinY) {
inputElt.scrollTop += cursorY - cursorMinY; inputElt.scrollTop += cursorY - cursorMinY;
} }
else if(cursorY > cursorMaxY) { else if(cursorY > cursorMaxY) {
inputElt.scrollTop += cursorY - cursorMaxY; inputElt.scrollTop += cursorY - cursorMaxY;
} }
}, 0); }, 0);
}
eventMgr.addListener('onLayoutResize', adjustCursorPosition); eventMgr.addListener('onLayoutResize', adjustCursorPosition);
editor.init = function(elt1, elt2) { editor.init = function(elt1, elt2) {
@ -279,7 +354,12 @@ define([
characterData: true characterData: true
}); });
$(inputElt).scroll(saveEditorState); $(inputElt).scroll(function() {
scrollTop = inputElt.scrollTop;
if(fileChanged === false) {
fileDesc.editorScrollTop = scrollTop;
}
});
$(previewElt).scroll(function() { $(previewElt).scroll(function() {
if(fileChanged === false) { if(fileChanged === false) {
fileDesc.previewScrollTop = previewElt.scrollTop; fileDesc.previewScrollTop = previewElt.scrollTop;
@ -303,64 +383,16 @@ define([
return this.textContent; return this.textContent;
}, },
set: function (value) { set: function (value) {
var currentValue = this.textContent; var contentChange = getContentChange(value);
var range = inputElt.createRange(contentChange.startIndex, contentChange.endIndex);
// Find the first modified char
var startIndex = 0;
var startIndexMax = Math.min(currentValue.length, value.length);
while (startIndex < startIndexMax) {
if (currentValue.charCodeAt(startIndex) !== value.charCodeAt(startIndex)) {
break;
}
startIndex++;
}
// Find the last modified char
var endIndex = 1;
var endIndexMax = Math.min(currentValue.length - startIndex, value.length - startIndex);
while (endIndex <= endIndexMax) {
if (currentValue.charCodeAt(currentValue.length - endIndex) !== value.charCodeAt(value.length - endIndex)) {
break;
}
endIndex++;
}
var replacementText = value.substring(startIndex, value.length - endIndex + 1);
endIndex = currentValue.length - endIndex + 1;
var range = inputElt.createRange(startIndex, endIndex);
range.deleteContents(); range.deleteContents();
range.insertNode(document.createTextNode(replacementText)); range.insertNode(document.createTextNode(contentChange.replacement));
} }
}); });
Object.defineProperty(inputElt, 'selectionStart', { Object.defineProperty(inputElt, 'selectionStart', {
get: function () { get: function () {
var selection = window.getSelection(); return selectionStart;
if (selection.rangeCount) {
var range = selection.getRangeAt(0),
element = range.startContainer,
container = element,
offset = range.startOffset;
if (!(this.compareDocumentPosition(element) & 0x10)) {
return selectionStart;
}
do {
while (element = element.previousSibling) {
if (element.textContent) {
offset += element.textContent.length;
}
}
element = container = container.parentNode;
} while (element && element != this);
return offset;
} else {
return selectionStart;
}
}, },
set: function (value) { set: function (value) {
inputElt.setSelectionStartEnd(value, selectionEnd); inputElt.setSelectionStartEnd(value, selectionEnd);
@ -372,13 +404,7 @@ define([
Object.defineProperty(inputElt, 'selectionEnd', { Object.defineProperty(inputElt, 'selectionEnd', {
get: function () { get: function () {
var selection = window.getSelection(); return selectionEnd;
if (selection.rangeCount) {
return this.selectionStart + (selection.getRangeAt(0) + '').length;
} else {
return selectionEnd;
}
}, },
set: function (value) { set: function (value) {
inputElt.setSelectionStartEnd(selectionStart, value); inputElt.setSelectionStartEnd(selectionStart, value);
@ -388,34 +414,24 @@ define([
configurable: true configurable: true
}); });
inputElt.setSelectionStartEnd = function (ss, se) { inputElt.setSelectionStartEnd = function (start, end) {
selectionStart = ss; selectionStart = start;
selectionEnd = se; selectionEnd = end;
var range = inputElt.createRange(ss, se); var range = inputElt.createRange(start, end);
var selection = window.getSelection(); var selection = window.getSelection();
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
}; };
inputElt.getSelectionStartEnd = function () { inputElt.createRange = function(start, end) {
return {
selectionStart: selectionStart,
selectionEnd: selectionEnd
};
};
inputElt.createRange = function(ss, se) {
var range = document.createRange(),
offset = _.isObject(ss) ? ss : findOffset(ss);
var range = document.createRange();
var offset = _.isObject(start) ? start : findOffset(start);
range.setStart(offset.element, offset.offset); range.setStart(offset.element, offset.offset);
if (se && se != ss) { if (end && end != start) {
offset = _.isObject(se) ? se : findOffset(se); offset = _.isObject(end) ? end : findOffset(end);
} }
range.setEnd(offset.element, offset.offset); range.setEnd(offset.element, offset.offset);
return range; return range;
}; };
@ -435,7 +451,7 @@ define([
) { ) {
return; return;
} }
saveEditorState(); saveSelectionState();
adjustCursorPosition(); adjustCursorPosition();
var cmdOrCtrl = evt.metaKey || evt.ctrlKey; var cmdOrCtrl = evt.metaKey || evt.ctrlKey;

View File

@ -210,6 +210,11 @@ define([
addEventHook("onSectionsCreated"); addEventHook("onSectionsCreated");
addEventHook("onCursorCoordinates"); addEventHook("onCursorCoordinates");
// Operations on comments
addEventHook("onDiscussionCreated");
addEventHook("onDiscussionRemoved");
addEventHook("onCommentAdded");
// Refresh twitter buttons // Refresh twitter buttons
addEventHook("onTweet"); addEventHook("onTweet");

View File

@ -2,18 +2,31 @@ define([
"jquery", "jquery",
"underscore", "underscore",
"utils", "utils",
"storage",
"crel", "crel",
"rangy", "rangy",
"classes/Extension", "classes/Extension",
"text!html/commentsPopoverContent.html", "text!html/commentsPopoverContent.html",
"bootstrap" "bootstrap"
], function($, _, utils, crel, rangy, Extension, commentsPopoverContentHTML) { ], function($, _, utils, storage, crel, rangy, Extension, commentsPopoverContentHTML) {
var comments = new Extension("comments", 'Comments'); var comments = new Extension("comments", 'Comments');
var commentTmpl = [
'<div class="comment-block">',
' <div class="comment-author"><%= author %></div>',
' <div class="comment-content"><%= content %></div>',
'</div>',
].join('');
var eventMgr;
comments.onEventMgrCreated = function(eventMgrParam) {
eventMgr = eventMgrParam;
};
var offsetMap = {}; var offsetMap = {};
function setCommentEltCoordinates(commentElt, y) { function setCommentEltCoordinates(commentElt, y) {
var lineIndex = Math.round(y/10); var lineIndex = Math.round(y / 10);
var top = (y - 8) + 'px'; var top = (y - 8) + 'px';
var right = ((offsetMap[lineIndex] || 0) * 25 + 10) + 'px'; var right = ((offsetMap[lineIndex] || 0) * 25 + 10) + 'px';
commentElt.style.top = top; commentElt.style.top = top;
@ -33,32 +46,28 @@ define([
setCommentEltCoordinates(newCommentElt, cursorY); setCommentEltCoordinates(newCommentElt, cursorY);
}; };
var fileDesc;
comments.onFileSelected = function(selectedFileDesc) {
fileDesc = selectedFileDesc;
refreshDiscussions();
};
var openedPopover;
comments.onLayoutResize = function() {
openedPopover && openedPopover.popover('toggle').popover('destroy');
refreshDiscussions();
};
var refreshId; var refreshId;
var currentFileDesc;
function refreshDiscussions() { function refreshDiscussions() {
if(currentFileDesc === undefined) {
return;
}
var author = storage['author.name'];
clearTimeout(refreshId); clearTimeout(refreshId);
commentEltList.forEach(function(commentElt) { commentEltList.forEach(function(commentElt) {
marginElt.removeChild(commentElt); marginElt.removeChild(commentElt);
}); });
commentEltList = []; commentEltList = [];
offsetMap = {}; offsetMap = {};
var discussionList = _.map(fileDesc.discussionList, _.identity); var discussionList = _.map(currentFileDesc.discussionList, _.identity);
function refreshOne() { function refreshOne() {
if(discussionList.length === 0) { var discussion;
return; do {
} if(discussionList.length === 0) {
var discussion = discussionList.pop(); return;
}
discussion = discussionList.pop();
} while(discussion.isRemoved);
var commentElt = crel('a', { var commentElt = crel('a', {
class: 'icon-comment' class: 'icon-comment'
}); });
@ -69,63 +78,115 @@ define([
marginElt.appendChild(commentElt); marginElt.appendChild(commentElt);
commentEltList.push(commentElt); commentEltList.push(commentElt);
// Replace newCommentElt // Move newCommentElt
setCommentEltCoordinates(newCommentElt, cursorY); setCommentEltCoordinates(newCommentElt, cursorY);
refreshId = setTimeout(refreshOne, 0); // 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);
} }
refreshId = setTimeout(refreshOne, 50); refreshId = setTimeout(refreshOne, 50);
} }
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('');
}
comments.onReady = function() { comments.onReady = function() {
var cssApplier = rangy.createCssClassApplier("comment-highlight", { var cssApplier = rangy.createCssClassApplier("comment-highlight", {
normalize: false normalize: false
}); });
var selectionRange; var previousContent = '';
var rangyRange;
var currentDiscussion;
inputElt = document.getElementById('wmd-input'); inputElt = document.getElementById('wmd-input');
marginElt = document.querySelector('#wmd-input > .editor-margin'); marginElt = document.querySelector('#wmd-input > .editor-margin');
marginElt.appendChild(newCommentElt); marginElt.appendChild(newCommentElt);
$(document.body).append(crel('div', { $(document.body).append(crel('div', {
class: 'comments-popover' class: 'comments-popover'
})).popover({ })).on('click', function(evt) {
// Close on click outside the popover
if(currentContext && currentContext.$commentElt[0] !== evt.target) {
closeCurrentPopover();
}
}).popover({
placement: 'auto top', placement: 'auto top',
container: '.comments-popover', container: '.comments-popover',
html: true, html: true,
title: function() { title: function() {
if(!currentDiscussion) { if(!currentContext) {
return '...'; return true;
} }
var titleLength = currentDiscussion.selectionEnd - currentDiscussion.selectionStart; var titleLength = currentContext.discussion.selectionEnd - currentContext.discussion.selectionStart;
var title = inputElt.textContent.substr(currentDiscussion.selectionStart, titleLength > 20 ? 20 : titleLength); var title = inputElt.textContent.substr(currentContext.discussion.selectionStart, titleLength > 20 ? 20 : titleLength);
if(titleLength > 20) { if(titleLength > 20) {
title += '...'; title += '...';
} }
return title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' '); title = title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
return '<a href="#" class="action-remove-discussion pull-right"><i class="icon-trash"></i></a>' + title;
}, },
content: function() { content: function() {
var content = _.template(commentsPopoverContentHTML, { var content = _.template(commentsPopoverContentHTML, {
commentList: getDiscussionComments()
}); });
return content; return content;
}, },
selector: '#wmd-input > .editor-margin > .icon-comment' selector: '#wmd-input > .editor-margin > .icon-comment'
}).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) {
$(evt.target).addClass('active'); closeCurrentPopover();
var context = {
$commentElt: $(evt.target).addClass('active')
};
currentContext = context;
inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
// If it's an existing discussion // If it's an existing discussion
if(evt.target.discussion) { if(evt.target.discussion) {
currentDiscussion = evt.target.discussion; context.discussion = evt.target.discussion;
selectionRange = inputElt.createRange(currentDiscussion.selectionStart, currentDiscussion.selectionEnd); context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd);
return; return;
} }
// Get selected text // Get selected text
var inputSelection = inputElt.getSelectionStartEnd(); var selectionStart = inputElt.selectionStart;
var selectionStart = inputSelection.selectionStart; var selectionEnd = inputElt.selectionEnd;
var selectionEnd = inputSelection.selectionEnd;
if(selectionStart === selectionEnd) { if(selectionStart === selectionEnd) {
var after = inputElt.textContent.substring(selectionStart); var after = inputElt.textContent.substring(selectionStart);
var match = /\S+/.exec(after); var match = /\S+/.exec(after);
@ -139,26 +200,30 @@ define([
selectionEnd += match.index + match[0].length; selectionEnd += match.index + match[0].length;
} }
} }
selectionRange = inputElt.createRange(selectionStart, selectionEnd); context.selectionRange = inputElt.createRange(selectionStart, selectionEnd);
currentDiscussion = { context.discussion = {
selectionStart: selectionStart, selectionStart: selectionStart,
selectionEnd: selectionEnd, selectionEnd: selectionEnd,
commentList: [] commentList: []
}; };
}).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) {
// Move the popover in the margin // Move the popover in the margin
var popoverElt = document.querySelector('.comments-popover .popover:last-child'); var context = currentContext;
var left = -10; context.popoverElt = document.querySelector('.comments-popover .popover:last-child');
if(popoverElt.offsetWidth < marginElt.offsetWidth) { var left = -5;
left = marginElt.offsetWidth - popoverElt.offsetWidth - 10; if(context.popoverElt.offsetWidth < marginElt.offsetWidth - 5) {
left = marginElt.offsetWidth - 10 - context.popoverElt.offsetWidth;
} }
popoverElt.style.left = left + 'px'; context.popoverElt.style.left = left + 'px';
popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(evt.target.style.right) - evt.target.offsetWidth / 2 - left) + 'px'; context.popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(evt.target.style.right) - evt.target.offsetWidth / 2 - left) + 'px';
var $textarea = $(popoverElt.querySelector('.input-comment-content')); // Scroll to the bottom of the discussion
var $addButton = $(popoverElt.querySelector('.action-add-comment')); context.popoverElt.querySelector('.popover-content').scrollTop = 9999999;
$textarea.keydown(function(evt) {
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) {
// Enter key // Enter key
switch(evt.which) { switch(evt.which) {
case 13: case 13:
@ -167,65 +232,106 @@ define([
return; return;
case 27: case 27:
evt.preventDefault(); evt.preventDefault();
openedPopover && openedPopover.popover('toggle').popover('destroy'); closeCurrentPopover();
inputElt.focus(); inputElt.focus();
return; return;
} }
}); });
$addButton.click(function(evt) { $addButton.click(function(evt) {
var author = utils.getInputTextValue(popoverElt.querySelector('.input-comment-author')); var author = utils.getInputTextValue(context.$authorInputElt);
var content = utils.getInputTextValue($textarea, evt); var content = utils.getInputTextValue(context.$contentInputElt, evt);
if(evt.isPropagationStopped()) { if(evt.isPropagationStopped()) {
return; return;
} }
var discussionList = fileDesc.discussionList || {}; context.$contentInputElt.val('');
if(!currentDiscussion.discussionIndex) { closeCurrentPopover();
var discussionList = currentFileDesc.discussionList || {};
var isNew = false;
if(!context.discussion.discussionIndex) {
isNew = true;
// Create discussion index // Create discussion index
var discussionIndex; var discussionIndex;
do { do {
discussionIndex = utils.randomString(); discussionIndex = utils.randomString();
} while(_.has(discussionList, discussionIndex)); } while(_.has(discussionList, discussionIndex));
currentDiscussion.discussionIndex = discussionIndex; context.discussion.discussionIndex = discussionIndex;
discussionList[discussionIndex] = currentDiscussion; discussionList[discussionIndex] = context.discussion;
} }
currentDiscussion.commentList.push({ context.discussion.commentList.push({
author: author, author: author,
content: content content: content
}); });
fileDesc.discussionList = discussionList; currentFileDesc.discussionList = discussionList; // Write discussionList in localStorage
openedPopover.popover('toggle').popover('destroy'); isNew ?
refreshDiscussions(); eventMgr.onDiscussionCreated(currentFileDesc, context.discussion) :
eventMgr.onCommentAdded(currentFileDesc, context.discussion);
inputElt.focus(); inputElt.focus();
}); });
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();
context.discussion.isRemoved = true;
delete context.discussion.selectionStart;
delete context.discussion.selectionEnd;
delete context.discussion.commentList;
currentFileDesc.discussionList = currentFileDesc.discussionList; // Write discussionList in localStorage
eventMgr.onDiscussionRemoved(currentFileDesc, context.discussion);
inputElt.focus();
});
}
else {
// Otherwise hide the remove button
$removeButton.hide();
}
// Prevent from closing on click inside the popover // Prevent from closing on click inside the popover
$(popoverElt).on('click', function(evt) { $(context.popoverElt).on('click', function(evt) {
evt.stopPropagation(); evt.stopPropagation();
}); });
setTimeout(function() {
openedPopover = $(evt.target);
// Highlight selected text // Highlight selected text
rangyRange = rangy.createRange(); context.rangyRange = rangy.createRange();
rangyRange.setStart(selectionRange.startContainer, selectionRange.startOffset); context.rangyRange.setStart(context.selectionRange.startContainer, context.selectionRange.startOffset);
rangyRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); context.rangyRange.setEnd(context.selectionRange.endContainer, context.selectionRange.endOffset);
cssApplier.applyToRange(rangyRange); setTimeout(function() { // Need to delay this because it's not refreshed properly
if(currentContext === context) {
cssApplier.applyToRange(context.rangyRange);
}
}, 50);
// Focus on textarea // Focus on textarea
$textarea.focus(); context.$contentInputElt.focus().val(previousContent);
}, 10);
}).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) {
$(evt.target).removeClass('active'); if(!currentContext) {
return;
}
currentContext.$commentElt.removeClass('active');
// Save content and author for later
previousContent = currentContext.$contentInputElt.val();
storage['author.name'] = currentContext.$authorInputElt.val();
// Remove highlight // Remove highlight
rangyRange && cssApplier.undoToRange(rangyRange); cssApplier.undoToRange(currentContext.rangyRange);
openedPopover = undefined; currentContext = undefined;
rangyRange = undefined;
currentDiscussion = undefined;
}).on('click', function() {
// Close on click outside the popover
openedPopover && openedPopover.popover('toggle').popover('destroy');
}); });
}; };

View File

@ -1,5 +1,5 @@
<div class="form-horizontal"> <div class="discussion-comment-list"><%= commentList %></div>
<div class="discussion-history"></div> <div class="new-comment-block">
<div class="form-group"> <div class="form-group">
<input class="form-control input-comment-author" placeholder="Your name"></input> <input class="form-control input-comment-author" placeholder="Your name"></input>
<textarea class="form-control input-comment-content"></textarea> <textarea class="form-control input-comment-content"></textarea>
@ -8,3 +8,11 @@
<button class="btn btn-primary action-add-comment">Add</button> <button class="btn btn-primary action-add-comment">Add</button>
</div> </div>
</div> </div>
<div class="remove-discussion-confirm hide">
<br/>
<blockquote>Remove this discussion, really?</blockquote>
<div class="form-group text-right">
<button class="btn btn-default action-remove-discussion-cancel">No</button>
<button class="btn btn-primary action-remove-discussion-confirm">Yes</button>
</div>
</div>

View File

@ -451,6 +451,18 @@ define([
}; };
function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) { function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) {
if(localDiscussion.isRemoved === true) {
realtimeDiscussion.set('isRemoved');
realtimeDiscussion.delete('selectionStart');
realtimeDiscussion.delete('selectionEnd');
return realtimeDiscussion.delete('commentList');
}
if(realtimeDiscussion.get('isRemoved') === true) {
localDiscussion.isRemoved = true;
delete localDiscussion.selectionStart;
delete localDiscussion.selectionEnd;
return delete localDiscussion.commentList;
}
if(takeServer) { if(takeServer) {
localDiscussion.selectionStart = realtimeDiscussion.get('selectionStart'); localDiscussion.selectionStart = realtimeDiscussion.get('selectionStart');
localDiscussion.selectionEnd = realtimeDiscussion.get('selectionEnd'); localDiscussion.selectionEnd = realtimeDiscussion.get('selectionEnd');

View File

@ -127,7 +127,7 @@
@popover-arrow-outer-color: @secondary-border-color; @popover-arrow-outer-color: @secondary-border-color;
@popover-title-bg: @transparent; @popover-title-bg: @transparent;
@alert-border-radius: 0; @alert-border-radius: 0;
@label-warning-bg: #d90; @label-warning-bg: #da0;
@label-danger-bg: #d00; @label-danger-bg: #d00;
@ -140,7 +140,7 @@ body {
} }
#preview-contents { #preview-contents {
padding: 30px; padding: 35px;
margin: 0 auto 200px; margin: 0 auto 200px;
text-align: justify; text-align: justify;
} }
@ -362,7 +362,7 @@ a {
.form-control.error { .form-control.error {
border-color: @error-border; border-color: @error-border;
.box-shadow(~"@{form-control-inset-shadow}, 0 0 8px rgba(255, 134, 97, 0.6)"); .box-shadow(~"@{form-control-inset-shadow}, 0 0 8px rgba(255, 0, 0, 0.6)");
} }
.help-block { .help-block {
@ -773,7 +773,7 @@ a {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
margin-top: 6px; margin-top: 6px;
right: 30px; right: 35px;
.ui-layout-resizer-south-closed & { .ui-layout-resizer-south-closed & {
display: none !important; display: none !important;
} }
@ -1060,22 +1060,28 @@ a {
top: 0; top: 0;
.icon-comment { .icon-comment {
&.new { &.new {
color: fade(@tertiary-color, 12%); color: fade(@tertiary-color, 10%);
&:hover { &:hover, &.active, &.active:hover {
color: fade(@tertiary-color, 35%) !important; color: fade(@tertiary-color, 35%) !important;
} }
} }
&.replied { &.replied {
color: fade(@label-danger-bg, 30%); color: fade(@label-danger-bg, 35%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: fade(@label-danger-bg, 45%) !important; color: fade(@label-danger-bg, 45%) !important;
} }
} }
&.added {
color: fade(@label-warning-bg, 40%);
&:hover, &.active, &.active:hover {
color: fade(@label-warning-bg, 60%) !important;
}
}
.transition(~"color ease-in-out .25s");
position: absolute; position: absolute;
color: fade(@label-warning-bg, 40%); color: fade(#fff, 0%);
cursor: pointer; cursor: pointer;
&:hover, &.active, &.active:hover { &:hover, &.active {
color: fade(@label-warning-bg, 60%) !important;
text-decoration: none; text-decoration: none;
} }
} }
@ -1399,7 +1405,7 @@ input[type="file"] {
*********************/ *********************/
.popover { .popover {
max-width: 240px; max-width: 230px;
padding: 10px 20px 0; padding: 10px 20px 0;
//.box-shadow(0 5px 30px rgba(0,0,0,.175)); //.box-shadow(0 5px 30px rgba(0,0,0,.175));
.popover-title { .popover-title {
@ -1408,6 +1414,10 @@ input[type="file"] {
padding: 5px 0 15px; padding: 5px 0 15px;
border-bottom: 1px solid @hr-border; border-bottom: 1px solid @hr-border;
line-height: @headings-line-height; line-height: @headings-line-height;
.action-remove-discussion {
font-size: 16px;
line-height: 22px;
}
} }
.icon-lock { .icon-lock {
font-size: 38px; font-size: 38px;
@ -1417,10 +1427,20 @@ input[type="file"] {
display: none; display: none;
} }
.popover-content { .popover-content {
padding-bottom: 0; padding: 10px 20px 0;
overflow: auto;
max-height: 230px;
margin: 0 -20px;
.btn { .btn {
padding: 6px 11px; padding: 6px 11px;
} }
.comment-block {
margin-bottom: 5px;
}
.comment-author {
padding-left: 12px;
font-weight: bold;
}
.input-comment-author { .input-comment-author {
border: none; border: none;
background: none; background: none;