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) {
return JSON.stringify(obj);
},
arrays: {
detectMove: false,
},
textDiff: {
minLength: 9999999
}
@ -82,25 +79,75 @@ define([
var merge = settings.conflictMode == 'merge';
Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionListJSON) {
var lineArray = [];
var lineHash = {};
function linesToChars(text) {
var chars = '';
var lineArrayLength = lineArray.length;
text.split('\n').forEach(function(line) {
if(lineHash.hasOwnProperty(line)) {
chars += String.fromCharCode(lineHash[line]);
} else {
chars += String.fromCharCode(lineArrayLength);
lineHash[line] = lineArrayLength;
lineArray[lineArrayLength++] = line;
function cleanupDiffs(diffs) {
var result = [];
var removeDiff = [-1, ''];
var addDiff = [1, ''];
var distance = 20;
diffs.forEach(function(diff) {
var diffType = diff[0];
var diffText = diff[1];
if(diffType === 0) {
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) {
if(!discussionList.length) {
return;
}
var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent);
var changed = false;
var startOffset = 0;
@ -121,20 +168,20 @@ define([
// selectionEnd
if(discussion.selectionEnd >= endOffset) {
discussion.selectionEnd += diffOffset;
changed = true;
discussion.discussionIndex && (changed = true);
}
else if(discussion.selectionEnd > startOffset) {
discussion.selectionEnd = startOffset;
changed = true;
discussion.discussionIndex && (changed = true);
}
// selectionStart
if(discussion.selectionStart >= endOffset) {
discussion.selectionStart += diffOffset;
changed = true;
discussion.discussionIndex && (changed = true);
}
else if(discussion.selectionStart > startOffset) {
discussion.selectionStart = startOffset;
changed = true;
discussion.discussionIndex && (changed = true);
}
});
startOffset = endOffset;
@ -157,28 +204,32 @@ define([
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON);
// Check content
var contentChanged = localContent != remoteContent;
var localContentChanged = syncAttributes.contentCRC != localContentCRC;
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var contentConflict = contentChanged && localContentChanged && remoteContentChanged;
contentChanged = contentChanged && remoteContentChanged;
var contentChanged = localContent != remoteContent && remoteContentChanged;
var contentConflict = contentChanged && localContentChanged;
// Check title
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
var titleChanged = localTitle != remoteTitle;
var localTitleChanged = syncAttributes.titleCRC != localTitleCRC;
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged;
titleChanged = titleChanged && remoteTitleChanged;
var titleChanged = localTitle != remoteTitle && remoteTitleChanged;
var titleConflict = titleChanged && localTitleChanged;
// Check discussionList
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON;
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged;
discussionListChanged = discussionListChanged && remoteDiscussionListChanged;
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON && remoteDiscussionListChanged;
var discussionListConflict = discussionListChanged && localDiscussionListChanged;
var conflictList = [];
var newContent = remoteContent;
var newTitle = remoteTitle;
var newDiscussionList = remoteDiscussionList;
var adjustLocalDiscussionList = false;
var adjustRemoteDiscussionList = false;
var mergeDiscussionList = false;
var diffs, patch;
if(
(!merge && (contentConflict || titleConflict || discussionListConflict)) ||
(contentConflict && syncAttributes.content === undefined) ||
@ -189,25 +240,21 @@ define([
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
}
else {
var oldDiscussionList;
var patch, delta;
if(contentConflict) {
// Patch content (line mode)
// Patch content
var oldContent = syncAttributes.content;
/*
var oldContentLines = linesToChars(syncAttributes.content);
var localContentLines = linesToChars(localContent);
var remoteContentLines = linesToChars(remoteContent);
*/
patch = diffMatchPatch.patch_make(oldContent, localContent);
diffs = diffMatchPatch.diff_main(oldContent, localContent);
diffMatchPatch.diff_cleanupSemantic(diffs);
patch = diffMatchPatch.patch_make(oldContent, diffs);
var patchResult = diffMatchPatch.patch_apply(patch, remoteContent);
var newContent = patchResult[0];
newContent = patchResult[0];
if(patchResult[1].some(function(patchSuccess) {
return !patchSuccess;
})) {
// Conflicts (some modifications have not been applied properly)
var diffs = diffMatchPatch.diff_main(localContent, newContent);
diffMatchPatch.diff_cleanupSemantic(diffs);
// Remaining conflicts
diffs = diffMatchPatch.diff_main(localContent, newContent);
diffs = cleanupDiffs(diffs);
newContent = '';
var conflict;
diffs.forEach(function(diff) {
@ -231,84 +278,105 @@ define([
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(localDiscussionListChanged || !remoteDiscussionListChanged) {
// Move local discussion according to content patch
var localDiscussionArray = _.values(localDiscussionList);
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray);
if(contentChanged) {
if(localDiscussionListChanged) {
adjustLocalDiscussionList = true;
}
if(remoteDiscussionListChanged) {
// Move remote discussion according to content patch
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);
}
adjustRemoteDiscussionList = true;
}
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
oldDiscussionList = JSON.parse(syncAttributes.discussionList);
delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
jsonDiffPatch.patch(remoteDiscussionList, delta);
if(discussionListConflict) {
mergeDiscussionList = true;
}
if(titleConflict) {
// Patch title
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) {
fileDesc.title = remoteTitle;
fileDesc.title = newTitle;
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) {
var self = this;
editor.watcher.noWatch(function() {
if(contentChanged) {
if(!/\n$/.test(remoteContent)) {
remoteContent += '\n';
if(!/\n$/.test(newContent)) {
newContent += '\n';
}
if(fileMgr.currentFile === fileDesc) {
editor.setValueNoWatch(remoteContent);
editor.setValueNoWatch(newContent);
editorSelection && editor.inputElt.setSelectionStartEnd(
editorSelection.selectionStart,
editorSelection.selectionEnd
);
}
fileDesc.content = remoteContent;
eventMgr.onContentChanged(fileDesc, remoteContent);
fileDesc.content = newContent;
eventMgr.onContentChanged(fileDesc, newContent);
}
if(discussionListChanged) {
fileDesc.discussionList = remoteDiscussionList;
@ -331,7 +399,7 @@ define([
editor.undoManager.saveState();
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + self.providerName + '.');
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 inputElt;
var $inputElt;
var contentElt;
var $contentElt;
var marginElt;
var $marginElt;
var previewElt;
var pagedownEditor;
var refreshPreviewLater = (function() {
@ -70,7 +74,7 @@ define([
this.startWatching = function() {
this.isWatching = true;
contentObserver = contentObserver || new MutationObserver(checkContentChange);
contentObserver.observe(editor.contentElt, {
contentObserver.observe(contentElt, {
childList: true,
subtree: true,
characterData: true
@ -113,11 +117,14 @@ define([
}
editor.setValueNoWatch = setValueNoWatch;
function setSelectionStartEnd(start, end) {
function setSelectionStartEnd(start, end, applySelection) {
selectionStart = start;
selectionEnd = end;
fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd;
if(applySelection === false) {
return;
}
var range = createRange(start, end);
var selection = window.getSelection();
selection.removeAllRanges();
@ -270,7 +277,7 @@ define([
};
this.currentMode = undefined;
lastMode = undefined;
editor.contentElt.textContent = content;
contentElt.textContent = content;
};
}
var undoManager = new UndoManager();
@ -393,7 +400,7 @@ define([
}
function findOffset(offset) {
var walker = document.createTreeWalker(editor.contentElt, 4);
var walker = document.createTreeWalker(contentElt, 4);
while(walker.nextNode()) {
var text = walker.currentNode.nodeValue || '';
if (text.length > offset) {
@ -405,7 +412,7 @@ define([
offset -= text.length;
}
return {
element: editor.contentElt,
element: contentElt,
offset: 0,
error: true
};
@ -505,20 +512,26 @@ define([
editor.init = function(elt1, elt2) {
inputElt = elt1;
$inputElt = $(inputElt);
editor.inputElt = inputElt;
editor.$inputElt = $inputElt;
previewElt = elt2;
editor.contentElt = crel('div', {
contentElt = crel('div', {
class: 'editor-content',
contenteditable: true
});
inputElt.appendChild(editor.contentElt);
editor.$contentElt = $(editor.contentElt);
inputElt.appendChild(contentElt);
editor.contentElt = contentElt;
$contentElt = $(contentElt);
editor.$contentElt = $contentElt;
editor.marginElt = crel('div', {
marginElt = crel('div', {
class: 'editor-margin'
});
inputElt.appendChild(editor.marginElt);
editor.$marginElt = $(editor.marginElt);
inputElt.appendChild(marginElt);
$marginElt = $(marginElt);
editor.$marginElt = $marginElt;
watcher.startWatching();
@ -535,14 +548,14 @@ define([
});
inputElt.focus = function() {
editor.$contentElt.focus();
$contentElt.focus();
setSelectionStartEnd(selectionStart, selectionEnd);
inputElt.scrollTop = scrollTop;
};
editor.$contentElt.focus(function() {
$contentElt.focus(function() {
inputElt.focused = true;
});
editor.$contentElt.blur(function() {
$contentElt.blur(function() {
inputElt.focused = false;
});
@ -585,7 +598,7 @@ define([
};
var clearNewline = false;
editor.$contentElt.on('keydown', function (evt) {
$contentElt.on('keydown', function (evt) {
if(
evt.which === 17 || // Ctrl
evt.which === 91 || // Cmd
@ -742,7 +755,7 @@ define([
// Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter ||
// 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
section.elt.textContent != newSection.textWithFrontMatter) {
leftIndex = index;
@ -758,7 +771,7 @@ define([
// Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter ||
// 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
section.elt.textContent != newSection.textWithFrontMatter) {
rightIndex = -index;
@ -789,30 +802,30 @@ define([
});
watcher.noWatch(function() {
if(fileChanged === true) {
editor.contentElt.innerHTML = '';
editor.contentElt.appendChild(newSectionEltList);
contentElt.innerHTML = '';
contentElt.appendChild(newSectionEltList);
setSelectionStartEnd(selectionStart, selectionEnd);
}
else {
// Remove outdated sections
sectionsToRemove.forEach(function(section) {
// 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) {
editor.contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt);
contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt);
}
else {
editor.contentElt.appendChild(newSectionEltList);
contentElt.appendChild(newSectionEltList);
}
// 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) {
var nextNode = childNode.nextSibling;
if(!childNode.generated) {
editor.contentElt.removeChild(childNode);
contentElt.removeChild(childNode);
}
childNode = nextNode;
}

View File

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

View File

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

View File

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

View File

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