Fixed sync merge

This commit is contained in:
benweet 2014-03-31 01:10:28 +01:00
parent 4ae6e540d4
commit b543e85779
6 changed files with 311 additions and 201 deletions

View File

@ -72,9 +72,6 @@ define([
objectHash: function(obj) { objectHash: function(obj) {
return JSON.stringify(obj); return JSON.stringify(obj);
}, },
arrays: {
detectMove: false,
},
textDiff: { textDiff: {
minLength: 9999999 minLength: 9999999
} }
@ -82,25 +79,75 @@ define([
var merge = settings.conflictMode == 'merge'; var merge = settings.conflictMode == 'merge';
Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionListJSON) { Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionListJSON) {
var lineArray = [];
var lineHash = {};
function linesToChars(text) { function cleanupDiffs(diffs) {
var chars = ''; var result = [];
var lineArrayLength = lineArray.length; var removeDiff = [-1, ''];
text.split('\n').forEach(function(line) { var addDiff = [1, ''];
if(lineHash.hasOwnProperty(line)) { var distance = 20;
chars += String.fromCharCode(lineHash[line]); diffs.forEach(function(diff) {
} else { var diffType = diff[0];
chars += String.fromCharCode(lineArrayLength); var diffText = diff[1];
lineHash[line] = lineArrayLength; if(diffType === 0) {
lineArray[lineArrayLength++] = line; if(diffText.length > distance) {
if(removeDiff[1] || addDiff[1]) {
var match = /\S+/.exec(diffText);
if(match) {
var prefixOffset = match.index + match[0].length;
var prefix = diffText.substring(0, prefixOffset);
diffText = diffText.substring(prefixOffset);
removeDiff[1] += prefix;
addDiff[1] += prefix;
}
}
if(diffText) {
var suffixOffset = diffText.length;
while(suffixOffset && /\S/.test(diffText[suffixOffset - 1])) {
suffixOffset--;
}
var suffix = diffText.substring(suffixOffset);
diffText = diffText.substring(0, suffixOffset);
if(diffText.length > distance) {
removeDiff[1] && result.push(removeDiff);
removeDiff = [-1, ''];
addDiff[1] && result.push(addDiff);
addDiff = [1, ''];
result.push([0, diffText]);
}
else {
removeDiff[1] += diffText;
addDiff[1] += diffText;
}
removeDiff[1] += suffix;
addDiff[1] += suffix;
}
}
else {
removeDiff[1] += diffText;
addDiff[1] += diffText;
}
}
else if(diffType === -1) {
removeDiff[1] += diffText;
}
else if(diffType === 1) {
addDiff[1] += diffText;
} }
}); });
return chars; if(removeDiff[1] == addDiff[1]) {
result.push([0, addDiff[1]]);
}
else {
removeDiff[1] && result.push(removeDiff);
addDiff[1] && result.push(addDiff);
}
return result;
} }
function moveComments(oldTextContent, newTextContent, discussionList) { function moveComments(oldTextContent, newTextContent, discussionList) {
if(!discussionList.length) {
return;
}
var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent); var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent);
var changed = false; var changed = false;
var startOffset = 0; var startOffset = 0;
@ -121,20 +168,20 @@ define([
// selectionEnd // selectionEnd
if(discussion.selectionEnd >= endOffset) { if(discussion.selectionEnd >= endOffset) {
discussion.selectionEnd += diffOffset; discussion.selectionEnd += diffOffset;
changed = true; discussion.discussionIndex && (changed = true);
} }
else if(discussion.selectionEnd > startOffset) { else if(discussion.selectionEnd > startOffset) {
discussion.selectionEnd = startOffset; discussion.selectionEnd = startOffset;
changed = true; discussion.discussionIndex && (changed = true);
} }
// selectionStart // selectionStart
if(discussion.selectionStart >= endOffset) { if(discussion.selectionStart >= endOffset) {
discussion.selectionStart += diffOffset; discussion.selectionStart += diffOffset;
changed = true; discussion.discussionIndex && (changed = true);
} }
else if(discussion.selectionStart > startOffset) { else if(discussion.selectionStart > startOffset) {
discussion.selectionStart = startOffset; discussion.selectionStart = startOffset;
changed = true; discussion.discussionIndex && (changed = true);
} }
}); });
startOffset = endOffset; startOffset = endOffset;
@ -157,28 +204,32 @@ define([
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON); var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON);
// Check content // Check content
var contentChanged = localContent != remoteContent;
var localContentChanged = syncAttributes.contentCRC != localContentCRC; var localContentChanged = syncAttributes.contentCRC != localContentCRC;
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var contentConflict = contentChanged && localContentChanged && remoteContentChanged; var contentChanged = localContent != remoteContent && remoteContentChanged;
contentChanged = contentChanged && remoteContentChanged; var contentConflict = contentChanged && localContentChanged;
// Check title // Check title
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
var titleChanged = localTitle != remoteTitle;
var localTitleChanged = syncAttributes.titleCRC != localTitleCRC; var localTitleChanged = syncAttributes.titleCRC != localTitleCRC;
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC; var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged; var titleChanged = localTitle != remoteTitle && remoteTitleChanged;
titleChanged = titleChanged && remoteTitleChanged; var titleConflict = titleChanged && localTitleChanged;
// Check discussionList // Check discussionList
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON;
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC; var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC; var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged; var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON && remoteDiscussionListChanged;
discussionListChanged = discussionListChanged && remoteDiscussionListChanged; var discussionListConflict = discussionListChanged && localDiscussionListChanged;
var conflictList = []; var conflictList = [];
var newContent = remoteContent;
var newTitle = remoteTitle;
var newDiscussionList = remoteDiscussionList;
var adjustLocalDiscussionList = false;
var adjustRemoteDiscussionList = false;
var mergeDiscussionList = false;
var diffs, patch;
if( if(
(!merge && (contentConflict || titleConflict || discussionListConflict)) || (!merge && (contentConflict || titleConflict || discussionListConflict)) ||
(contentConflict && syncAttributes.content === undefined) || (contentConflict && syncAttributes.content === undefined) ||
@ -189,25 +240,21 @@ define([
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
} }
else { else {
var oldDiscussionList;
var patch, delta;
if(contentConflict) { if(contentConflict) {
// Patch content (line mode) // Patch content
var oldContent = syncAttributes.content; var oldContent = syncAttributes.content;
/* diffs = diffMatchPatch.diff_main(oldContent, localContent);
var oldContentLines = linesToChars(syncAttributes.content); diffMatchPatch.diff_cleanupSemantic(diffs);
var localContentLines = linesToChars(localContent); patch = diffMatchPatch.patch_make(oldContent, diffs);
var remoteContentLines = linesToChars(remoteContent);
*/
patch = diffMatchPatch.patch_make(oldContent, localContent);
var patchResult = diffMatchPatch.patch_apply(patch, remoteContent); var patchResult = diffMatchPatch.patch_apply(patch, remoteContent);
var newContent = patchResult[0]; newContent = patchResult[0];
if(patchResult[1].some(function(patchSuccess) { if(patchResult[1].some(function(patchSuccess) {
return !patchSuccess; return !patchSuccess;
})) { })) {
// Conflicts (some modifications have not been applied properly) // Remaining conflicts
var diffs = diffMatchPatch.diff_main(localContent, newContent); diffs = diffMatchPatch.diff_main(localContent, newContent);
diffMatchPatch.diff_cleanupSemantic(diffs); diffs = cleanupDiffs(diffs);
newContent = ''; newContent = '';
var conflict; var conflict;
diffs.forEach(function(diff) { diffs.forEach(function(diff) {
@ -231,84 +278,105 @@ define([
conflictList.push(conflict); conflictList.push(conflict);
} }
} }
/* }
remoteContentLines = diffMatchPatch.patch_apply(patch, remoteContentLines)[0];
var newContent = remoteContentLines.split('').map(function(char) {
return lineArray[char.charCodeAt(0)];
}).join('\n');
*/
// Whether we take the local discussionList into account if(contentChanged) {
if(localDiscussionListChanged || !remoteDiscussionListChanged) { if(localDiscussionListChanged) {
// Move local discussion according to content patch adjustLocalDiscussionList = true;
var localDiscussionArray = _.values(localDiscussionList);
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray);
} }
if(remoteDiscussionListChanged) { if(remoteDiscussionListChanged) {
// Move remote discussion according to content patch adjustRemoteDiscussionList = true;
var remoteDiscussionArray = _.values(remoteDiscussionList);
moveComments(remoteContent, newContent, remoteDiscussionArray);
if(localDiscussionListChanged) {
// Patch remote discussionList with local modifications
oldDiscussionList = JSON.parse(syncAttributes.discussionList);
delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
jsonDiffPatch.patch(remoteDiscussionList, delta);
}
} }
else { else {
remoteDiscussionList = localDiscussionList; adjustLocalDiscussionList = true;
newDiscussionList = localDiscussionList;
} }
if(conflictList.length) {
discussionListChanged = true;
// Add conflicts to discussionList
conflictList.forEach(function(conflict) {
// Create discussion index
var discussionIndex;
do {
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
} while(_.has(remoteDiscussionList, discussionIndex));
conflict.discussionIndex = discussionIndex;
remoteDiscussionList[discussionIndex] = conflict;
});
}
remoteContent = newContent;
} }
else if(discussionListConflict) {
// Patch remote discussionList with local modifications if(discussionListConflict) {
oldDiscussionList = JSON.parse(syncAttributes.discussionList); mergeDiscussionList = true;
delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
jsonDiffPatch.patch(remoteDiscussionList, delta);
} }
if(titleConflict) { if(titleConflict) {
// Patch title // Patch title
patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle); patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle);
remoteTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0]; newTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0];
} }
} }
// Adjust local discussions offset
var editorSelection;
if(contentChanged) {
var localDiscussionArray = [];
// Adjust editor's cursor position and local discussions at the same time
if(fileMgr.currentFile === fileDesc) {
editorSelection = {
selectionStart: editor.inputElt.selectionStart,
selectionEnd: editor.inputElt.selectionEnd
};
localDiscussionArray.push(editorSelection);
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
}
if(adjustLocalDiscussionList) {
localDiscussionArray = localDiscussionArray.concat(_.values(localDiscussionList));
}
discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray);
}
// Adjust remote discussions offset
if(adjustRemoteDiscussionList) {
var remoteDiscussionArray = _.values(remoteDiscussionList);
moveComments(remoteContent, newContent, remoteDiscussionArray);
}
// Patch remote discussionList with local modifications
if(mergeDiscussionList) {
var oldDiscussionList = JSON.parse(syncAttributes.discussionList);
diffs = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
jsonDiffPatch.patch(remoteDiscussionList, diffs);
_.each(remoteDiscussionList, function(discussion, discussionIndex) {
if(!discussion) {
delete remoteDiscussionList[discussionIndex];
}
});
}
if(conflictList.length) {
discussionListChanged = true;
// Add conflicts to discussionList
conflictList.forEach(function(conflict) {
// Create discussion index
var discussionIndex;
do {
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
} while(_.has(newDiscussionList, discussionIndex));
conflict.discussionIndex = discussionIndex;
newDiscussionList[discussionIndex] = conflict;
});
}
if(titleChanged) { if(titleChanged) {
fileDesc.title = remoteTitle; fileDesc.title = newTitle;
eventMgr.onTitleChanged(fileDesc); eventMgr.onTitleChanged(fileDesc);
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.'); eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + newTitle + '" on ' + this.providerName + '.');
} }
if(contentChanged || discussionListChanged) { if(contentChanged || discussionListChanged) {
var self = this; var self = this;
editor.watcher.noWatch(function() { editor.watcher.noWatch(function() {
if(contentChanged) { if(contentChanged) {
if(!/\n$/.test(remoteContent)) { if(!/\n$/.test(newContent)) {
remoteContent += '\n'; newContent += '\n';
} }
if(fileMgr.currentFile === fileDesc) { if(fileMgr.currentFile === fileDesc) {
editor.setValueNoWatch(remoteContent); editor.setValueNoWatch(newContent);
editorSelection && editor.inputElt.setSelectionStartEnd(
editorSelection.selectionStart,
editorSelection.selectionEnd
);
} }
fileDesc.content = remoteContent; fileDesc.content = newContent;
eventMgr.onContentChanged(fileDesc, remoteContent); eventMgr.onContentChanged(fileDesc, newContent);
} }
if(discussionListChanged) { if(discussionListChanged) {
fileDesc.discussionList = remoteDiscussionList; fileDesc.discussionList = remoteDiscussionList;
@ -331,7 +399,7 @@ define([
editor.undoManager.saveState(); editor.undoManager.saveState();
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + self.providerName + '.'); eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + self.providerName + '.');
if(conflictList.length) { if(conflictList.length) {
eventMgr.onMessage('"' + remoteTitle + '" contains conflicts you need to review.'); eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.');
} }
}); });
} }

View File

@ -24,6 +24,10 @@ define([
var scrollTop = 0; var scrollTop = 0;
var inputElt; var inputElt;
var $inputElt; var $inputElt;
var contentElt;
var $contentElt;
var marginElt;
var $marginElt;
var previewElt; var previewElt;
var pagedownEditor; var pagedownEditor;
var refreshPreviewLater = (function() { var refreshPreviewLater = (function() {
@ -70,7 +74,7 @@ define([
this.startWatching = function() { this.startWatching = function() {
this.isWatching = true; this.isWatching = true;
contentObserver = contentObserver || new MutationObserver(checkContentChange); contentObserver = contentObserver || new MutationObserver(checkContentChange);
contentObserver.observe(editor.contentElt, { contentObserver.observe(contentElt, {
childList: true, childList: true,
subtree: true, subtree: true,
characterData: true characterData: true
@ -113,11 +117,14 @@ define([
} }
editor.setValueNoWatch = setValueNoWatch; editor.setValueNoWatch = setValueNoWatch;
function setSelectionStartEnd(start, end) { function setSelectionStartEnd(start, end, applySelection) {
selectionStart = start; selectionStart = start;
selectionEnd = end; selectionEnd = end;
fileDesc.editorStart = selectionStart; fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd; fileDesc.editorEnd = selectionEnd;
if(applySelection === false) {
return;
}
var range = createRange(start, end); var range = createRange(start, end);
var selection = window.getSelection(); var selection = window.getSelection();
selection.removeAllRanges(); selection.removeAllRanges();
@ -270,7 +277,7 @@ define([
}; };
this.currentMode = undefined; this.currentMode = undefined;
lastMode = undefined; lastMode = undefined;
editor.contentElt.textContent = content; contentElt.textContent = content;
}; };
} }
var undoManager = new UndoManager(); var undoManager = new UndoManager();
@ -393,7 +400,7 @@ define([
} }
function findOffset(offset) { function findOffset(offset) {
var walker = document.createTreeWalker(editor.contentElt, 4); var walker = document.createTreeWalker(contentElt, 4);
while(walker.nextNode()) { while(walker.nextNode()) {
var text = walker.currentNode.nodeValue || ''; var text = walker.currentNode.nodeValue || '';
if (text.length > offset) { if (text.length > offset) {
@ -405,7 +412,7 @@ define([
offset -= text.length; offset -= text.length;
} }
return { return {
element: editor.contentElt, element: contentElt,
offset: 0, offset: 0,
error: true error: true
}; };
@ -505,20 +512,26 @@ define([
editor.init = function(elt1, elt2) { editor.init = function(elt1, elt2) {
inputElt = elt1; inputElt = elt1;
$inputElt = $(inputElt); $inputElt = $(inputElt);
editor.inputElt = inputElt;
editor.$inputElt = $inputElt;
previewElt = elt2; previewElt = elt2;
editor.contentElt = crel('div', { contentElt = crel('div', {
class: 'editor-content', class: 'editor-content',
contenteditable: true contenteditable: true
}); });
inputElt.appendChild(editor.contentElt); inputElt.appendChild(contentElt);
editor.$contentElt = $(editor.contentElt); editor.contentElt = contentElt;
$contentElt = $(contentElt);
editor.$contentElt = $contentElt;
editor.marginElt = crel('div', { marginElt = crel('div', {
class: 'editor-margin' class: 'editor-margin'
}); });
inputElt.appendChild(editor.marginElt); inputElt.appendChild(marginElt);
editor.$marginElt = $(editor.marginElt); $marginElt = $(marginElt);
editor.$marginElt = $marginElt;
watcher.startWatching(); watcher.startWatching();
@ -535,14 +548,14 @@ define([
}); });
inputElt.focus = function() { inputElt.focus = function() {
editor.$contentElt.focus(); $contentElt.focus();
setSelectionStartEnd(selectionStart, selectionEnd); setSelectionStartEnd(selectionStart, selectionEnd);
inputElt.scrollTop = scrollTop; inputElt.scrollTop = scrollTop;
}; };
editor.$contentElt.focus(function() { $contentElt.focus(function() {
inputElt.focused = true; inputElt.focused = true;
}); });
editor.$contentElt.blur(function() { $contentElt.blur(function() {
inputElt.focused = false; inputElt.focused = false;
}); });
@ -585,7 +598,7 @@ define([
}; };
var clearNewline = false; var clearNewline = false;
editor.$contentElt.on('keydown', function (evt) { $contentElt.on('keydown', function (evt) {
if( if(
evt.which === 17 || // Ctrl evt.which === 17 || // Ctrl
evt.which === 91 || // Cmd evt.which === 91 || // Cmd
@ -742,7 +755,7 @@ define([
// Check modified // Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter || section.textWithFrontMatter != newSection.textWithFrontMatter ||
// Check that section has not been detached or moved // Check that section has not been detached or moved
section.elt.parentNode !== editor.contentElt || section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste // Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent != newSection.textWithFrontMatter) { section.elt.textContent != newSection.textWithFrontMatter) {
leftIndex = index; leftIndex = index;
@ -758,7 +771,7 @@ define([
// Check modified // Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter || section.textWithFrontMatter != newSection.textWithFrontMatter ||
// Check that section has not been detached or moved // Check that section has not been detached or moved
section.elt.parentNode !== editor.contentElt || section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste // Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent != newSection.textWithFrontMatter) { section.elt.textContent != newSection.textWithFrontMatter) {
rightIndex = -index; rightIndex = -index;
@ -789,30 +802,30 @@ define([
}); });
watcher.noWatch(function() { watcher.noWatch(function() {
if(fileChanged === true) { if(fileChanged === true) {
editor.contentElt.innerHTML = ''; contentElt.innerHTML = '';
editor.contentElt.appendChild(newSectionEltList); contentElt.appendChild(newSectionEltList);
setSelectionStartEnd(selectionStart, selectionEnd); setSelectionStartEnd(selectionStart, selectionEnd);
} }
else { else {
// Remove outdated sections // Remove outdated sections
sectionsToRemove.forEach(function(section) { sectionsToRemove.forEach(function(section) {
// section can be already removed // section can be already removed
section.elt.parentNode === editor.contentElt && editor.contentElt.removeChild(section.elt); section.elt.parentNode === contentElt && contentElt.removeChild(section.elt);
}); });
if(insertBeforeSection !== undefined) { if(insertBeforeSection !== undefined) {
editor.contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt); contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt);
} }
else { else {
editor.contentElt.appendChild(newSectionEltList); contentElt.appendChild(newSectionEltList);
} }
// Remove unauthorized nodes (text nodes outside of sections or duplicated sections via copy/paste) // Remove unauthorized nodes (text nodes outside of sections or duplicated sections via copy/paste)
var childNode = editor.contentElt.firstChild; var childNode = contentElt.firstChild;
while(childNode) { while(childNode) {
var nextNode = childNode.nextSibling; var nextNode = childNode.nextSibling;
if(!childNode.generated) { if(!childNode.generated) {
editor.contentElt.removeChild(childNode); contentElt.removeChild(childNode);
} }
childNode = nextNode; childNode = nextNode;
} }

View File

@ -13,8 +13,8 @@ define([
var comments = new Extension("comments", 'Comments'); var comments = new Extension("comments", 'Comments');
var commentTmpl = [ var commentTmpl = [
'<div class="comment-block">', '<div class="comment-block<%= reply ? \' reply\' : \'\' %>">',
' <div class="comment-author"><%= author %></div>', ' <div class="comment-author"><i class="icon-comment"></i> <%= author %></div>',
' <div class="comment-content"><%= content %></div>', ' <div class="comment-content"><%= content %></div>',
'</div>', '</div>',
].join(''); ].join('');
@ -23,7 +23,7 @@ define([
' <a href="#" class="action-remove-discussion pull-right<%= !type ? \'\': \' hide\' %>">', ' <a href="#" class="action-remove-discussion pull-right<%= !type ? \'\': \' hide\' %>">',
' <i class="icon-trash"></i>', ' <i class="icon-trash"></i>',
' </a>', ' </a>',
' <%- title %>', ' <%- title %>',
'</span>', '</span>',
].join(''); ].join('');
@ -35,7 +35,7 @@ define([
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 yOffset = -8; var yOffset = -10;
if(commentElt.className.indexOf(' icon-fork') !== -1) { if(commentElt.className.indexOf(' icon-fork') !== -1) {
yOffset = -12; yOffset = -12;
} }
@ -48,9 +48,8 @@ define([
var inputElt; var inputElt;
var marginElt; var marginElt;
var commentEltList = [];
var newCommentElt = crel('a', { var newCommentElt = crel('a', {
class: 'discussion icon-comment new' class: 'discussion icon-quote-left new'
}); });
var cursorY; var cursorY;
comments.onCursorCoordinates = function(x, y) { comments.onCursorCoordinates = function(x, y) {
@ -58,39 +57,56 @@ define([
setCommentEltCoordinates(newCommentElt, cursorY); setCommentEltCoordinates(newCommentElt, cursorY);
}; };
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');
};
var currentContext; var currentContext;
function movePopover(commentElt) { function movePopover(commentElt) {
// Move popover in the margin // Move popover in the margin
var context = currentContext; var popoverElt = currentContext.getPopoverElt();
context.popoverElt = document.querySelector('.comments-popover .popover:last-child');
var left = 0; var left = 0;
if(context.popoverElt.offsetWidth < marginElt.offsetWidth - 10) { if(popoverElt.offsetWidth < marginElt.offsetWidth - 10) {
left = marginElt.offsetWidth - 10 - context.popoverElt.offsetWidth; left = marginElt.offsetWidth - 10 - popoverElt.offsetWidth;
} }
context.popoverElt.style.left = left + 'px'; popoverElt.style.left = left + 'px';
context.popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(commentElt.style.right) - commentElt.offsetWidth / 2 - left) + 'px'; popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(commentElt.style.right) - commentElt.offsetWidth / 2 - left) + 'px';
} }
var cssApplier; var cssApplier;
var currentFileDesc; var currentFileDesc;
var refreshTimeoutId; var refreshTimeoutId;
var commentEltMap = {};
var refreshDiscussions = _.debounce(function() { var refreshDiscussions = _.debounce(function() {
if(currentFileDesc === undefined) { if(currentFileDesc === undefined) {
return; return;
} }
var author = storage['author.name']; var author = storage['author.name'];
commentEltList.forEach(function(commentElt) {
marginElt.removeChild(commentElt);
});
commentEltList = [];
offsetMap = {}; offsetMap = {};
var discussionList = _.values(currentFileDesc.discussionList); var discussionList = _.values(currentFileDesc.discussionList);
function refreshOne() { function refreshOne() {
if(discussionList.length === 0) { if(discussionList.length === 0) {
// Remove outdated commentElt
_.filter(commentEltMap, function(commentElt, discussionIndex) {
return !_.has(currentFileDesc.discussionList, discussionIndex);
}).forEach(function(commentElt) {
marginElt.removeChild(commentElt);
});
// Move newCommentElt // Move newCommentElt
setCommentEltCoordinates(newCommentElt, cursorY); setCommentEltCoordinates(newCommentElt, cursorY);
if(currentContext && !currentContext.discussion.discussionIndex) { if(currentContext && !currentContext.discussionIndex) {
inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
movePopover(newCommentElt); movePopover(newCommentElt);
} }
@ -105,16 +121,19 @@ define([
} }
else { else {
var isReplied = _.last(discussion.commentList).author != author; var isReplied = _.last(discussion.commentList).author != author;
commentElt.className += ' icon-comment' + (isReplied ? ' replied' : ' added'); commentElt.className += ' icon-quote-left' + (isReplied ? ' replied' : ' added');
} }
commentElt.discussionIndex = discussion.discussionIndex; commentElt.discussionIndex = discussion.discussionIndex;
var coordinates = inputElt.getOffsetCoordinates(discussion.selectionEnd); var coordinates = inputElt.getOffsetCoordinates(discussion.selectionEnd);
var lineIndex = setCommentEltCoordinates(commentElt, coordinates.y); var lineIndex = setCommentEltCoordinates(commentElt, coordinates.y);
offsetMap[lineIndex] = (offsetMap[lineIndex] || 0) + 1; offsetMap[lineIndex] = (offsetMap[lineIndex] || 0) + 1;
marginElt.appendChild(commentElt);
commentEltList.push(commentElt);
if(currentContext && currentContext.discussion == discussion) { var oldCommentElt = commentEltMap[discussion.discussionIndex];
oldCommentElt && marginElt.removeChild(oldCommentElt);
marginElt.appendChild(commentElt);
commentEltMap[discussion.discussionIndex] = commentElt;
if(currentContext && currentContext.getDiscussion() == discussion) {
inputElt.scrollTop += parseInt(commentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; inputElt.scrollTop += parseInt(commentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
movePopover(commentElt); movePopover(commentElt);
} }
@ -140,15 +159,15 @@ define([
if(currentContext !== undefined) { if(currentContext !== undefined) {
// Refresh conversation if popover is open // Refresh conversation if popover is open
var context = currentContext; var context = currentContext;
if(context.discussion.discussionIndex) { if(context.discussionIndex) {
context.discussion = currentFileDesc.discussionList[context.discussion.discussionIndex];
context.popoverElt.querySelector('.discussion-comment-list').innerHTML = getDiscussionComments(); context.popoverElt.querySelector('.discussion-comment-list').innerHTML = getDiscussionComments();
} }
try { try {
cssApplier.undoToRange(context.rangyRange); cssApplier.undoToRange(context.rangyRange);
} }
catch(e) {} catch(e) {}
context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd); var discussion = context.getDiscussion();
context.selectionRange = inputElt.createRange(discussion.selectionStart, discussion.selectionEnd);
// Highlight selected text // Highlight selected text
context.rangyRange = rangy.createRange(); context.rangyRange = rangy.createRange();
@ -174,7 +193,7 @@ define([
comments.onDiscussionRemoved = function(fileDesc, discussion) { comments.onDiscussionRemoved = function(fileDesc, discussion) {
if(currentFileDesc === fileDesc) { if(currentFileDesc === fileDesc) {
// Close popover if the discussion has been removed // Close popover if the discussion has been removed
if(currentContext !== undefined && currentContext.discussion.discussionIndex == discussion.discussionIndex) { if(currentContext !== undefined && currentContext.discussionIndex == discussion.discussionIndex) {
closeCurrentPopover(); closeCurrentPopover();
} }
refreshDiscussions(); refreshDiscussions();
@ -186,13 +205,17 @@ define([
}; };
function getDiscussionComments() { function getDiscussionComments() {
if(currentContext.discussion.type == 'conflict') { var discussion = currentContext.getDiscussion();
var author = storage['author.name'];
if(discussion.type == 'conflict') {
return ''; return '';
} }
return currentContext.discussion.commentList.map(function(comment) { return discussion.commentList.map(function(comment) {
var commentAuthor = comment.author || 'Anonymous';
return _.template(commentTmpl, { return _.template(commentTmpl, {
author: comment.author || 'Anonymous', author: commentAuthor,
content: comment.content content: comment.content,
reply: comment.author != author
}); });
}).join(''); }).join('');
} }
@ -221,37 +244,36 @@ define([
if(!currentContext) { if(!currentContext) {
return true; return true;
} }
var titleLength = currentContext.discussion.selectionEnd - currentContext.discussion.selectionStart; var discussion = currentContext.getDiscussion();
var title = inputElt.textContent.substr(currentContext.discussion.selectionStart, titleLength > 20 ? 20 : titleLength); var titleLength = discussion.selectionEnd - discussion.selectionStart;
var title = inputElt.textContent.substr(discussion.selectionStart, titleLength > 20 ? 20 : titleLength);
if(titleLength > 20) { if(titleLength > 20) {
title += '...'; title += '...';
} }
return _.template(popoverTitleTmpl, { return _.template(popoverTitleTmpl, {
title: title, title: title,
type: currentContext.discussion.type type: discussion.type
}); });
}, },
content: function() { content: function() {
var content = _.template(commentsPopoverContentHTML, { var content = _.template(commentsPopoverContentHTML, {
commentList: getDiscussionComments(), commentList: getDiscussionComments(),
type: currentContext.discussion.type type: currentContext.getDiscussion().type
}); });
return content; return content;
}, },
selector: '#wmd-input > .editor-margin > .discussion' selector: '#wmd-input > .editor-margin > .discussion'
}).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) {
closeCurrentPopover(); closeCurrentPopover();
var context = { var context = new Context(evt.target, currentFileDesc);
$commentElt: $(evt.target).addClass('active'),
fileDesc: currentFileDesc
};
currentContext = context; 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.discussionIndex) { var discussion = context.getDiscussion();
context.discussion = currentFileDesc.discussionList[evt.target.discussionIndex]; if(discussion) {
context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd); context.selectionRange = inputElt.createRange(discussion.selectionStart, discussion.selectionEnd);
inputElt.setSelectionStartEnd(discussion.selectionStart, discussion.selectionEnd, false);
return; return;
} }
@ -272,24 +294,23 @@ define([
} }
} }
context.selectionRange = inputElt.createRange(selectionStart, selectionEnd); context.selectionRange = inputElt.createRange(selectionStart, selectionEnd);
context.discussion = { currentFileDesc.newDiscussion = {
selectionStart: selectionStart, selectionStart: selectionStart,
selectionEnd: selectionEnd, selectionEnd: selectionEnd,
commentList: [] commentList: []
}; };
currentFileDesc.newDiscussion = context.discussion;
}).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) {
var context = currentContext; var context = currentContext;
context.popoverElt = document.querySelector('.comments-popover .popover:last-child'); movePopover(context.commentElt);
movePopover(evt.target); var popoverElt = context.getPopoverElt();
// Scroll to the bottom of the discussion // Scroll to the bottom of the discussion
context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; popoverElt.querySelector('.popover-content').scrollTop = 9999999;
context.$authorInputElt = $(context.popoverElt.querySelector('.input-comment-author')).val(storage['author.name']); context.$authorInputElt = $(popoverElt.querySelector('.input-comment-author')).val(storage['author.name']);
context.$contentInputElt = $(context.popoverElt.querySelector('.input-comment-content')); context.$contentInputElt = $(popoverElt.querySelector('.input-comment-content'));
var $addButton = $(context.popoverElt.querySelector('.action-add-comment')); var $addButton = $(popoverElt.querySelector('.action-add-comment'));
context.$contentInputElt.keydown(function(evt) { $().add(context.$contentInputElt).add(context.$authorInputElt).keydown(function(evt) {
// Enter key // Enter key
switch(evt.which) { switch(evt.which) {
case 13: case 13:
@ -310,24 +331,25 @@ define([
return; return;
} }
var discussion = context.getDiscussion();
context.$contentInputElt.val(''); context.$contentInputElt.val('');
closeCurrentPopover(); closeCurrentPopover();
context.discussion.commentList.push({ discussion.commentList.push({
author: author, author: author,
content: content content: content
}); });
var discussionList = context.fileDesc.discussionList || {}; var discussionList = context.fileDesc.discussionList || {};
if(!context.discussion.discussionIndex) { if(!discussion.discussionIndex) {
// Create discussion index // Create discussion index
var discussionIndex; var discussionIndex;
do { do {
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
} while(_.has(discussionList, discussionIndex)); } while(_.has(discussionList, discussionIndex));
context.discussion.discussionIndex = discussionIndex; discussion.discussionIndex = discussionIndex;
discussionList[discussionIndex] = context.discussion; discussionList[discussionIndex] = discussion;
context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
eventMgr.onDiscussionCreated(context.fileDesc, context.discussion); eventMgr.onDiscussionCreated(context.fileDesc, discussion);
} }
else { else {
context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
@ -336,33 +358,28 @@ define([
inputElt.focus(); inputElt.focus();
}); });
var $removeButton = $(context.popoverElt.querySelector('.action-remove-discussion')); var $removeButton = $(popoverElt.querySelector('.action-remove-discussion'));
if(evt.target.discussionIndex) { if(evt.target.discussionIndex) {
// If it's an existing discussion // If it's an existing discussion
/* var $removeCancelButton = $(popoverElt.querySelectorAll('.action-remove-discussion-cancel'));
if(context.discussion.type == 'conflict') { var $removeConfirmButton = $(popoverElt.querySelectorAll('.action-remove-discussion-confirm'));
$(context.popoverElt.querySelector('.conflict-review')).removeClass('hide');
$(context.popoverElt.querySelector('.new-comment-block')).addClass('hide');
}
*/
var $removeCancelButton = $(context.popoverElt.querySelectorAll('.action-remove-discussion-cancel'));
var $removeConfirmButton = $(context.popoverElt.querySelectorAll('.action-remove-discussion-confirm'));
$removeButton.click(function() { $removeButton.click(function() {
$(context.popoverElt.querySelector('.new-comment-block')).addClass('hide'); $(popoverElt.querySelector('.new-comment-block')).addClass('hide');
$(context.popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide'); $(popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide');
context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; popoverElt.querySelector('.popover-content').scrollTop = 9999999;
}); });
$removeCancelButton.click(function() { $removeCancelButton.click(function() {
$(context.popoverElt.querySelector('.new-comment-block')).removeClass('hide'); $(popoverElt.querySelector('.new-comment-block')).removeClass('hide');
$(context.popoverElt.querySelector('.remove-discussion-confirm')).addClass('hide'); $(popoverElt.querySelector('.remove-discussion-confirm')).addClass('hide');
context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; popoverElt.querySelector('.popover-content').scrollTop = 9999999;
context.$contentInputElt.focus(); context.$contentInputElt.focus();
}); });
$removeConfirmButton.click(function() { $removeConfirmButton.click(function() {
closeCurrentPopover(); closeCurrentPopover();
delete context.fileDesc.discussionList[context.discussion.discussionIndex]; var discussion = context.getDiscussion();
delete context.fileDesc.discussionList[discussion.discussionIndex];
context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage
eventMgr.onCommentsChanged(context.fileDesc); eventMgr.onDiscussionRemoved(context.fileDesc, discussion);
inputElt.focus(); inputElt.focus();
}); });
} }
@ -372,7 +389,7 @@ define([
} }
// Prevent from closing on click inside the popover // Prevent from closing on click inside the popover
$(context.popoverElt).on('click', function(evt) { $(popoverElt).on('click', function(evt) {
evt.stopPropagation(); evt.stopPropagation();
}); });

View File

@ -1,7 +1,7 @@
<div class="discussion-comment-list"><%= commentList %></div> <div class="discussion-comment-list"><%= commentList %></div>
<div class="new-comment-block<%= !type ? '': ' hide' %>"> <div class="new-comment-block<%= !type ? '': ' hide' %>">
<div class="form-group"> <div class="form-group">
<input class="form-control input-comment-author" placeholder="Your name"></input> <i class="icon-comment"></i> <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>
</div> </div>
<div class="form-group text-right"> <div class="form-group text-right">

View File

@ -129,6 +129,7 @@ define([
syncAttributes.discussionList = discussionList; syncAttributes.discussionList = discussionList;
} }
syncAttributes.contentCRC = contentCRC; syncAttributes.contentCRC = contentCRC;
syncAttributes.titleCRC = titleCRC; // Not synchronized but has to be there for syncMerge
syncAttributes.discussionListCRC = discussionListCRC; syncAttributes.discussionListCRC = discussionListCRC;
callback(undefined, true); callback(undefined, true);

View File

@ -94,7 +94,7 @@
@list-group-active-border: fade(@secondary, 5%); @list-group-active-border: fade(@secondary, 5%);
@list-group-hover-bg: @btn-default-hover-bg; @list-group-hover-bg: @btn-default-hover-bg;
@list-group-hover-border-color: fade(@secondary, 10%); @list-group-hover-border-color: fade(@secondary, 10%);
@input-color: @secondary-color-dark; @input-color: @secondary-color-darkest;
@input-color-placeholder: @disabled-color; @input-color-placeholder: @disabled-color;
@btn-default-color: @secondary-color-darker; @btn-default-color: @secondary-color-darker;
@btn-default-bg: @transparent; @btn-default-bg: @transparent;
@ -1059,6 +1059,9 @@ a {
top: 0; top: 0;
.discussion { .discussion {
font-size: 18px; font-size: 18px;
&:before {
margin-right: 0;
}
&.new { &.new {
color: fade(@tertiary-color, 10%); color: fade(@tertiary-color, 10%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
@ -1072,7 +1075,7 @@ a {
} }
} }
&.replied { &.replied {
color: fade(@label-danger-bg, 45%); color: fade(@label-danger-bg, 55%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: fade(@label-danger-bg, 80%) !important; color: fade(@label-danger-bg, 80%) !important;
} }
@ -1083,9 +1086,6 @@ a {
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: @label-danger-bg !important; color: @label-danger-bg !important;
} }
&:before {
margin-right: 0;
}
} }
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
@ -1094,7 +1094,7 @@ a {
} }
} }
} }
&.has-selection > .editor-margin .icon-comment.new { &.has-selection > .editor-margin .discussion.new {
color: fade(@tertiary-color, 25%); color: fade(@tertiary-color, 25%);
} }
@ -1423,6 +1423,7 @@ 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;
overflow: hidden;
.action-remove-discussion { .action-remove-discussion {
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
@ -1447,15 +1448,25 @@ input[type="file"] {
margin-bottom: 5px; margin-bottom: 5px;
} }
.comment-author { .comment-author {
padding-left: 12px;
font-weight: bold; font-weight: bold;
color: @input-color;
}
.icon-comment {
font-size: 14px;
color: fade(@label-warning-bg, 60%);
}
.reply .icon-comment {
color: fade(@label-danger-bg, 70%);
} }
.input-comment-author { .input-comment-author {
border: none; border: none;
background: none; background: none;
.box-shadow(none); .box-shadow(none);
font-weight: bold; font-weight: bold;
height: 32px; height: 28px;
padding: 0 0 5px;
width: 150px;
display: inline-block;
} }
} }