Fixed sync merge
This commit is contained in:
parent
4ae6e540d4
commit
b543e85779
@ -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,35 +278,67 @@ 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);
|
||||
if(contentChanged) {
|
||||
if(localDiscussionListChanged) {
|
||||
adjustLocalDiscussionList = true;
|
||||
}
|
||||
if(remoteDiscussionListChanged) {
|
||||
adjustRemoteDiscussionList = true;
|
||||
}
|
||||
else {
|
||||
adjustLocalDiscussionList = true;
|
||||
newDiscussionList = localDiscussionList;
|
||||
}
|
||||
}
|
||||
|
||||
if(discussionListConflict) {
|
||||
mergeDiscussionList = true;
|
||||
}
|
||||
|
||||
if(titleConflict) {
|
||||
// Patch title
|
||||
patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle);
|
||||
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);
|
||||
}
|
||||
|
||||
if(remoteDiscussionListChanged) {
|
||||
// Move remote discussion according to content patch
|
||||
// Adjust remote discussions offset
|
||||
if(adjustRemoteDiscussionList) {
|
||||
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);
|
||||
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];
|
||||
}
|
||||
}
|
||||
else {
|
||||
remoteDiscussionList = localDiscussionList;
|
||||
});
|
||||
}
|
||||
|
||||
if(conflictList.length) {
|
||||
@ -270,45 +349,34 @@ define([
|
||||
var discussionIndex;
|
||||
do {
|
||||
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
|
||||
} while(_.has(remoteDiscussionList, discussionIndex));
|
||||
} while(_.has(newDiscussionList, discussionIndex));
|
||||
conflict.discussionIndex = discussionIndex;
|
||||
remoteDiscussionList[discussionIndex] = conflict;
|
||||
newDiscussionList[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(titleConflict) {
|
||||
// Patch title
|
||||
patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle);
|
||||
remoteTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0];
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user