Fixed sync merge

This commit is contained in:
benweet 2014-03-30 02:44:51 +01:00
parent acebad8a65
commit 4ae6e540d4
12 changed files with 449 additions and 339 deletions

View File

@ -4,9 +4,10 @@ define([
'settings', 'settings',
'eventMgr', 'eventMgr',
'fileMgr', 'fileMgr',
'editor',
'diff_match_patch_uncompressed', 'diff_match_patch_uncompressed',
'jsondiffpatch', 'jsondiffpatch',
], function(_, utils, settings, eventMgr, fileMgr, diff_match_patch, jsondiffpatch) { ], function(_, utils, settings, eventMgr, fileMgr, editor, diff_match_patch, jsondiffpatch) {
function Provider(providerId, providerName) { function Provider(providerId, providerName) {
this.providerId = providerId; this.providerId = providerId;
@ -26,6 +27,9 @@ define([
) { ) {
throw 'invalid'; throw 'invalid';
} }
if(discussion.type == 'conflict') {
return;
}
discussion.commentList.forEach(function(comment) { discussion.commentList.forEach(function(comment) {
if( if(
(!_.isString(comment.author)) || (!_.isString(comment.author)) ||
@ -42,7 +46,7 @@ define([
}; };
Provider.prototype.serializeContent = function(content, discussionList) { Provider.prototype.serializeContent = function(content, discussionList) {
if(_.size(discussionList) !== 0) { if(discussionList.length > 2) { // It's a serialized JSON
return content + '<!--se_discussion_list:' + discussionList + '-->'; return content + '<!--se_discussion_list:' + discussionList + '-->';
} }
return content; return content;
@ -62,6 +66,8 @@ define([
}; };
var diffMatchPatch = new diff_match_patch(); var diffMatchPatch = new diff_match_patch();
diffMatchPatch.Match_Threshold = 0;
diffMatchPatch.Patch_DeleteThreshold = 0;
var jsonDiffPatch = jsondiffpatch.create({ var jsonDiffPatch = jsondiffpatch.create({
objectHash: function(obj) { objectHash: function(obj) {
return JSON.stringify(obj); return JSON.stringify(obj);
@ -96,7 +102,7 @@ define([
function moveComments(oldTextContent, newTextContent, discussionList) { function moveComments(oldTextContent, newTextContent, discussionList) {
var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent); var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent);
var updateDiscussionList = false; var changed = false;
var startOffset = 0; var startOffset = 0;
changes.forEach(function(change) { changes.forEach(function(change) {
var changeType = change[0]; var changeType = change[0];
@ -115,30 +121,32 @@ define([
// selectionEnd // selectionEnd
if(discussion.selectionEnd >= endOffset) { if(discussion.selectionEnd >= endOffset) {
discussion.selectionEnd += diffOffset; discussion.selectionEnd += diffOffset;
updateDiscussionList = true; changed = true;
} }
else if(discussion.selectionEnd > startOffset) { else if(discussion.selectionEnd > startOffset) {
discussion.selectionEnd = startOffset; discussion.selectionEnd = startOffset;
updateDiscussionList = true; changed = true;
} }
// selectionStart // selectionStart
if(discussion.selectionStart >= endOffset) { if(discussion.selectionStart >= endOffset) {
discussion.selectionStart += diffOffset; discussion.selectionStart += diffOffset;
updateDiscussionList = true; changed = true;
} }
else if(discussion.selectionStart > startOffset) { else if(discussion.selectionStart > startOffset) {
discussion.selectionStart = startOffset; discussion.selectionStart = startOffset;
updateDiscussionList = true; changed = true;
} }
}); });
startOffset = endOffset; startOffset = endOffset;
}); });
return updateDiscussionList; return changed;
} }
var localContent = fileDesc.content; var localContent = fileDesc.content;
var localTitle = fileDesc.title; var localTitle = fileDesc.title;
var localDiscussionListJSON = fileDesc.discussionListJSON; var localDiscussionListJSON = fileDesc.discussionListJSON;
var localDiscussionList = fileDesc.discussionList;
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
// Local/Remote CRCs // Local/Remote CRCs
var localContentCRC = utils.crc32(localContent); var localContentCRC = utils.crc32(localContent);
@ -153,6 +161,7 @@ define([
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 contentConflict = contentChanged && localContentChanged && remoteContentChanged;
contentChanged = contentChanged && remoteContentChanged;
// Check title // Check title
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
@ -160,14 +169,16 @@ define([
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 titleConflict = titleChanged && localTitleChanged && remoteTitleChanged;
titleChanged = titleChanged && remoteTitleChanged;
// Check discussionList // Check discussionList
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON; 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 discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged;
discussionListChanged = discussionListChanged && remoteDiscussionListChanged;
// Conflict detection var conflictList = [];
if( if(
(!merge && (contentConflict || titleConflict || discussionListConflict)) || (!merge && (contentConflict || titleConflict || discussionListConflict)) ||
(contentConflict && syncAttributes.content === undefined) || (contentConflict && syncAttributes.content === undefined) ||
@ -178,28 +189,61 @@ 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 updateDiscussionList = remoteDiscussionListChanged;
var localDiscussionList = fileDesc.discussionList;
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
var oldDiscussionList; var oldDiscussionList;
var patch, delta; var patch, delta;
if(contentConflict) { if(contentConflict) {
// Patch content (line mode) // Patch content (line mode)
var oldContent = syncAttributes.content;
/*
var oldContentLines = linesToChars(syncAttributes.content); var oldContentLines = linesToChars(syncAttributes.content);
var localContentLines = linesToChars(localContent); var localContentLines = linesToChars(localContent);
var remoteContentLines = linesToChars(remoteContent); var remoteContentLines = linesToChars(remoteContent);
patch = diffMatchPatch.patch_make(oldContentLines, localContentLines); */
patch = diffMatchPatch.patch_make(oldContent, localContent);
var patchResult = diffMatchPatch.patch_apply(patch, remoteContent);
var 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);
newContent = '';
var conflict;
diffs.forEach(function(diff) {
var diffType = diff[0];
var diffText = diff[1];
if(diffType !== 0 && !conflict) {
conflict = {
selectionStart: newContent.length,
type: 'conflict'
};
}
else if(diffType === 0 && conflict) {
conflict.selectionEnd = newContent.length;
conflictList.push(conflict);
conflict = undefined;
}
newContent += diffText;
});
if(conflict) {
conflict.selectionEnd = newContent.length;
conflictList.push(conflict);
}
}
/*
remoteContentLines = diffMatchPatch.patch_apply(patch, remoteContentLines)[0]; remoteContentLines = diffMatchPatch.patch_apply(patch, remoteContentLines)[0];
var newContent = remoteContentLines.split('').map(function(char) { var newContent = remoteContentLines.split('').map(function(char) {
return lineArray[char.charCodeAt(0)]; return lineArray[char.charCodeAt(0)];
}).join('\n'); }).join('\n');
*/
// Whether we take the local discussionList into account // Whether we take the local discussionList into account
if(localDiscussionListChanged || !remoteDiscussionListChanged) { if(localDiscussionListChanged || !remoteDiscussionListChanged) {
// Move local discussion according to content patch // Move local discussion according to content patch
var localDiscussionArray = _.values(localDiscussionList); var localDiscussionArray = _.values(localDiscussionList);
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion); fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
updateDiscussionList |= moveComments(localContent, newContent, localDiscussionArray); discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray);
} }
if(remoteDiscussionListChanged) { if(remoteDiscussionListChanged) {
@ -217,6 +261,21 @@ define([
else { else {
remoteDiscussionList = localDiscussionList; remoteDiscussionList = 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; remoteContent = newContent;
} }
else if(discussionListConflict) { else if(discussionListConflict) {
@ -232,21 +291,57 @@ define([
} }
} }
if(titleChanged && remoteTitleChanged) { if(titleChanged) {
fileDesc.title = remoteTitle; fileDesc.title = remoteTitle;
eventMgr.onTitleChanged(fileDesc); eventMgr.onTitleChanged(fileDesc);
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.'); eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.');
} }
if(contentChanged && remoteContentChanged === true) {
if(fileMgr.currentFile === fileDesc) { if(contentChanged || discussionListChanged) {
document.getElementById('wmd-input').setValueSilently(remoteContent); var self = this;
editor.watcher.noWatch(function() {
if(contentChanged) {
if(!/\n$/.test(remoteContent)) {
remoteContent += '\n';
}
if(fileMgr.currentFile === fileDesc) {
editor.setValueNoWatch(remoteContent);
} }
else {
fileDesc.content = remoteContent; fileDesc.content = remoteContent;
eventMgr.onContentChanged(fileDesc, remoteContent); eventMgr.onContentChanged(fileDesc, remoteContent);
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + this.providerName + '.');
} }
if(discussionListChanged) {
fileDesc.discussionList = remoteDiscussionList;
var diff = jsonDiffPatch.diff(localDiscussionList, remoteDiscussionList);
var commentsChanged = false;
_.each(diff, function(discussionDiff, discussionIndex) {
if(!_.isArray(discussionDiff)) {
commentsChanged = true;
} }
else if(discussionDiff.length === 1) {
eventMgr.onDiscussionCreated(fileDesc, remoteDiscussionList[discussionIndex]);
}
else {
eventMgr.onDiscussionRemoved(fileDesc, localDiscussionList[discussionIndex]);
}
});
commentsChanged && eventMgr.onCommentsChanged(fileDesc);
}
editor.undoManager.currentMode = 'sync';
editor.undoManager.saveState();
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + self.providerName + '.');
if(conflictList.length) {
eventMgr.onMessage('"' + remoteTitle + '" contains conflicts you need to review.');
}
});
}
// Return remote CRCs
return {
contentCRC: remoteContentCRC,
titleCRC: remoteTitleCRC,
discussionListCRC: remoteDiscussionListCRC
};
}; };
return Provider; return Provider;

View File

@ -63,23 +63,76 @@ define([
fileDesc = selectedFileDesc; fileDesc = selectedFileDesc;
}); });
// Watcher used to detect editor changes
function Watcher() {
this.isWatching = false;
var contentObserver; var contentObserver;
var isWatching = false; this.startWatching = function() {
function noWatch(cb) { this.isWatching = true;
if(isWatching === true) { contentObserver = contentObserver || new MutationObserver(checkContentChange);
contentObserver.disconnect();
isWatching = false;
cb();
isWatching = true;
contentObserver.observe(editor.contentElt, { contentObserver.observe(editor.contentElt, {
childList: true, childList: true,
subtree: true, subtree: true,
characterData: true characterData: true
}); });
};
this.stopWatching = function() {
contentObserver.disconnect();
this.isWatching = false;
};
this.noWatch = function(cb) {
if(this.isWatching === true) {
this.stopWatching();
cb();
this.startWatching();
} }
else { else {
cb(); cb();
} }
};
}
var watcher = new Watcher();
editor.watcher = watcher;
function setValue(value) {
var startOffset = diffMatchPatch.diff_commonPrefix(previousTextContent, value);
var endOffset = Math.min(
diffMatchPatch.diff_commonSuffix(previousTextContent, value),
previousTextContent.length - startOffset,
value.length - startOffset
);
var replacement = value.substring(startOffset, value.length - endOffset);
var range = createRange(startOffset, previousTextContent.length - endOffset);
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
}
function setValueNoWatch(value) {
setValue(value);
previousTextContent = value;
}
editor.setValueNoWatch = setValueNoWatch;
function setSelectionStartEnd(start, end) {
selectionStart = start;
selectionEnd = end;
fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd;
var range = createRange(start, end);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function createRange(start, end) {
var range = document.createRange();
var offset = _.isObject(start) ? start : findOffset(start);
range.setStart(offset.element, offset.offset);
if (end && end != start) {
offset = _.isObject(end) ? end : findOffset(end);
}
range.setEnd(offset.element, offset.offset);
return range;
} }
var diffMatchPatch = new diff_match_patch(); var diffMatchPatch = new diff_match_patch();
@ -96,11 +149,7 @@ define([
}); });
var previousTextContent; var previousTextContent;
var currentMode; function UndoManager() {
editor.undoManager = (function() {
var undoManager = {
onButtonStateChange: function() {}
};
var undoStack = []; var undoStack = [];
var redoStack = []; var redoStack = [];
var lastTime; var lastTime;
@ -108,14 +157,15 @@ define([
var currentState; var currentState;
var selectionStartBefore; var selectionStartBefore;
var selectionEndBefore; var selectionEndBefore;
undoManager.setCommandMode = function() { this.setCommandMode = function() {
currentMode = 'command'; this.currentMode = 'command';
}; };
undoManager.setMode = function() {}; // For compatibility with PageDown this.setMode = function() {}; // For compatibility with PageDown
undoManager.saveState = function() { this.onButtonStateChange = function() {}; // To be overridden by PageDown
this.saveState = function() {
redoStack = []; redoStack = [];
var currentTime = Date.now(); var currentTime = Date.now();
if(currentMode == 'comment' || (currentMode != lastMode && lastMode != 'newlines') || currentTime - lastTime > 1000) { if(this.currentMode == 'comment' || (this.currentMode != lastMode && lastMode != 'newlines') || currentTime - lastTime > 1000) {
undoStack.push(currentState); undoStack.push(currentState);
// Limit the size of the stack // Limit the size of the stack
if(undoStack.length === 100) { if(undoStack.length === 100) {
@ -135,32 +185,34 @@ define([
discussionListJSON: fileDesc.discussionListJSON discussionListJSON: fileDesc.discussionListJSON
}; };
lastTime = currentTime; lastTime = currentTime;
lastMode = currentMode; lastMode = this.currentMode;
currentMode = undefined; this.currentMode = undefined;
undoManager.onButtonStateChange(); this.onButtonStateChange();
}; };
undoManager.saveSelectionState = _.debounce(function() { this.saveSelectionState = _.debounce(function() {
if(currentMode === undefined) { if(this.currentMode === undefined) {
selectionStartBefore = selectionStart; selectionStartBefore = selectionStart;
selectionEndBefore = selectionEnd; selectionEndBefore = selectionEnd;
} }
}, 10); }, 10);
undoManager.canUndo = function() { this.canUndo = function() {
return undoStack.length; return undoStack.length;
}; };
undoManager.canRedo = function() { this.canRedo = function() {
return redoStack.length; return redoStack.length;
}; };
var self = this;
function restoreState(state, selectionStart, selectionEnd) { function restoreState(state, selectionStart, selectionEnd) {
// Update editor // Update editor
noWatch(function() { watcher.noWatch(function() {
if(previousTextContent != state.content) { if(previousTextContent != state.content) {
inputElt.setValueSilently(state.content); setValueNoWatch(state.content);
fileDesc.content = state.content;
eventMgr.onContentChanged(fileDesc, state.content);
} }
inputElt.setSelectionStartEnd(selectionStart, selectionEnd); setSelectionStartEnd(selectionStart, selectionEnd);
var discussionListJSON = fileDesc.discussionListJSON; var discussionListJSON = fileDesc.discussionListJSON;
if(discussionListJSON != state.discussionListJSON) { if(discussionListJSON != state.discussionListJSON) {
currentMode = 'undoredo'; // In order to avoid saveState
var oldDiscussionList = fileDesc.discussionList; var oldDiscussionList = fileDesc.discussionList;
fileDesc.discussionListJSON = state.discussionListJSON; fileDesc.discussionListJSON = state.discussionListJSON;
var newDiscussionList = fileDesc.discussionList; var newDiscussionList = fileDesc.discussionList;
@ -184,12 +236,12 @@ define([
selectionStartBefore = selectionStart; selectionStartBefore = selectionStart;
selectionEndBefore = selectionEnd; selectionEndBefore = selectionEnd;
currentState = state; currentState = state;
currentMode = undefined; self.currentMode = undefined;
lastMode = undefined; lastMode = undefined;
undoManager.onButtonStateChange(); self.onButtonStateChange();
adjustCursorPosition(); adjustCursorPosition();
} }
undoManager.undo = function() { this.undo = function() {
var state = undoStack.pop(); var state = undoStack.pop();
if(!state) { if(!state) {
return; return;
@ -197,7 +249,7 @@ define([
redoStack.push(currentState); redoStack.push(currentState);
restoreState(state, currentState.selectionStartBefore, currentState.selectionEndBefore); restoreState(state, currentState.selectionStartBefore, currentState.selectionEndBefore);
}; };
undoManager.redo = function() { this.redo = function() {
var state = redoStack.pop(); var state = redoStack.pop();
if(!state) { if(!state) {
return; return;
@ -205,7 +257,7 @@ define([
undoStack.push(currentState); undoStack.push(currentState);
restoreState(state, state.selectionStartAfter, state.selectionEndAfter); restoreState(state, state.selectionStartAfter, state.selectionEndAfter);
}; };
undoManager.init = function() { this.init = function() {
var content = fileDesc.content; var content = fileDesc.content;
undoStack = []; undoStack = [];
redoStack = []; redoStack = [];
@ -216,17 +268,18 @@ define([
content: content, content: content,
discussionListJSON: fileDesc.discussionListJSON discussionListJSON: fileDesc.discussionListJSON
}; };
currentMode = undefined; this.currentMode = undefined;
lastMode = undefined; lastMode = undefined;
editor.contentElt.textContent = content; editor.contentElt.textContent = content;
}; };
return undoManager; }
})(); var undoManager = new UndoManager();
editor.undoManager = undoManager;
function onComment() { function onComment() {
if(!currentMode) { if(watcher.isWatching === true) {
currentMode = 'comment'; undoManager.currentMode = 'comment';
editor.undoManager.saveState(); undoManager.saveState();
} }
} }
eventMgr.addListener('onDiscussionCreated', onComment); eventMgr.addListener('onDiscussionCreated', onComment);
@ -259,7 +312,7 @@ define([
fileDesc.editorStart = selectionStart; fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd; fileDesc.editorEnd = selectionEnd;
} }
editor.undoManager.saveSelectionState(); undoManager.saveSelectionState();
} }
function checkContentChange() { function checkContentChange() {
@ -272,7 +325,7 @@ define([
if(!/\n$/.test(currentTextContent)) { if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n'; currentTextContent += '\n';
} }
currentMode = currentMode || 'typing'; undoManager.currentMode = undoManager.currentMode || 'typing';
var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent); var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent);
// Move comments according to changes // Move comments according to changes
var updateDiscussionList = false; var updateDiscussionList = false;
@ -321,7 +374,7 @@ define([
eventMgr.onContentChanged(fileDesc, currentTextContent); eventMgr.onContentChanged(fileDesc, currentTextContent);
updateDiscussionList && eventMgr.onCommentsChanged(fileDesc); updateDiscussionList && eventMgr.onCommentsChanged(fileDesc);
previousTextContent = currentTextContent; previousTextContent = currentTextContent;
editor.undoManager.saveState(); undoManager.saveState();
} }
else { else {
if(!/\n$/.test(currentTextContent)) { if(!/\n$/.test(currentTextContent)) {
@ -339,56 +392,18 @@ define([
} }
} }
function findOffset(ss) { function findOffset(offset) {
var offset = 0, var walker = document.createTreeWalker(editor.contentElt, 4);
element = editor.contentElt, while(walker.nextNode()) {
container; var text = walker.currentNode.nodeValue || '';
if (text.length > offset) {
do {
container = element;
element = element.firstChild;
if (element) {
do {
var len = element.textContent.length;
if (offset <= ss && offset + len > ss) {
break;
}
offset += len;
} while (element = element.nextSibling);
}
if (!element) {
// It's the container's lastChild
break;
}
} while (element && element.hasChildNodes() && element.nodeType != 3);
if (element) {
return { return {
element: element, element: walker.currentNode,
offset: ss - offset offset: offset
};
} else if (container) {
element = container;
while (element && element.lastChild) {
element = element.lastChild;
}
if (element.nodeType === 3) {
return {
element: element,
offset: element.textContent.length
};
} else {
return {
element: element,
offset: 0
}; };
} }
offset -= text.length;
} }
return { return {
element: editor.contentElt, element: editor.contentElt,
offset: 0, offset: 0,
@ -406,13 +421,13 @@ define([
var selectedChar = inputElt.textContent[inputOffset]; var selectedChar = inputElt.textContent[inputOffset];
var selectionRange; var selectionRange;
if(selectedChar === undefined || selectedChar == '\n') { if(selectedChar === undefined || selectedChar == '\n') {
selectionRange = inputElt.createRange(inputOffset - 1, { selectionRange = createRange(inputOffset - 1, {
element: element, element: element,
offset: offset offset: offset
}); });
} }
else { else {
selectionRange = inputElt.createRange({ selectionRange = createRange({
element: element, element: element,
offset: offset offset: offset
}, inputOffset + 1); }, inputOffset + 1);
@ -505,13 +520,7 @@ define([
inputElt.appendChild(editor.marginElt); inputElt.appendChild(editor.marginElt);
editor.$marginElt = $(editor.marginElt); editor.$marginElt = $(editor.marginElt);
contentObserver = new MutationObserver(checkContentChange); watcher.startWatching();
isWatching = true;
contentObserver.observe(editor.contentElt, {
childList: true,
subtree: true,
characterData: true
});
$(inputElt).scroll(function() { $(inputElt).scroll(function() {
scrollTop = inputElt.scrollTop; scrollTop = inputElt.scrollTop;
@ -527,7 +536,7 @@ define([
inputElt.focus = function() { inputElt.focus = function() {
editor.$contentElt.focus(); editor.$contentElt.focus();
this.setSelectionStartEnd(selectionStart, selectionEnd); setSelectionStartEnd(selectionStart, selectionEnd);
inputElt.scrollTop = scrollTop; inputElt.scrollTop = scrollTop;
}; };
editor.$contentElt.focus(function() { editor.$contentElt.focus(function() {
@ -541,38 +550,15 @@ define([
get: function () { get: function () {
return this.textContent; return this.textContent;
}, },
set: function (value) { set: setValue
var startOffset = diffMatchPatch.diff_commonPrefix(previousTextContent, value);
var endOffset = Math.min(
diffMatchPatch.diff_commonSuffix(previousTextContent, value),
previousTextContent.length - startOffset,
value.length - startOffset
);
var replacement = value.substring(startOffset, value.length - endOffset);
var range = inputElt.createRange(startOffset, previousTextContent.length - endOffset);
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
}
}); });
inputElt.setValueSilently = function(value) {
noWatch(function() {
if(!/\n$/.test(value)) {
value += '\n';
}
inputElt.value = value;
fileDesc.content = value;
previousTextContent = value;
eventMgr.onContentChanged(fileDesc, value);
});
};
Object.defineProperty(inputElt, 'selectionStart', { Object.defineProperty(inputElt, 'selectionStart', {
get: function () { get: function () {
return selectionStart; return selectionStart;
}, },
set: function (value) { set: function (value) {
inputElt.setSelectionStartEnd(value, selectionEnd); setSelectionStartEnd(value, selectionEnd);
}, },
enumerable: true, enumerable: true,
@ -584,37 +570,15 @@ define([
return selectionEnd; return selectionEnd;
}, },
set: function (value) { set: function (value) {
inputElt.setSelectionStartEnd(selectionStart, value); setSelectionStartEnd(selectionStart, value);
}, },
enumerable: true, enumerable: true,
configurable: true configurable: true
}); });
inputElt.setSelectionStartEnd = function(start, end) { inputElt.setSelectionStartEnd = setSelectionStartEnd;
selectionStart = start; inputElt.createRange = createRange;
selectionEnd = end;
fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd;
var range = inputElt.createRange(start, end);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
};
inputElt.createRange = function(start, end) {
var range = document.createRange();
var offset = _.isObject(start) ? start : findOffset(start);
range.setStart(offset.element, offset.offset);
if (end && end != start) {
offset = _.isObject(end) ? end : findOffset(end);
}
range.setEnd(offset.element, offset.offset);
return range;
};
inputElt.getOffsetCoordinates = function(ss) { inputElt.getOffsetCoordinates = function(ss) {
var offset = findOffset(ss); var offset = findOffset(ss);
return getCoordinates(ss, offset.element, offset.offset); return getCoordinates(ss, offset.element, offset.offset);
@ -631,9 +595,11 @@ define([
return; return;
} }
saveSelectionState(); saveSelectionState();
adjustCursorPosition();
var cmdOrCtrl = evt.metaKey || evt.ctrlKey; var cmdOrCtrl = evt.metaKey || evt.ctrlKey;
if(!cmdOrCtrl) {
adjustCursorPosition();
}
switch (evt.which) { switch (evt.which) {
case 9: // Tab case 9: // Tab
@ -659,11 +625,11 @@ define([
}, 0); }, 0);
}) })
.on('paste', function () { .on('paste', function () {
currentMode = 'paste'; undoManager.currentMode = 'paste';
adjustCursorPosition(); adjustCursorPosition();
}) })
.on('cut', function () { .on('cut', function () {
currentMode = 'cut'; undoManager.currentMode = 'cut';
adjustCursorPosition(); adjustCursorPosition();
}); });
@ -683,7 +649,7 @@ define([
actions[action](state, options); actions[action](state, options);
inputElt.value = state.before + state.selection + state.after; inputElt.value = state.before + state.selection + state.after;
inputElt.setSelectionStartEnd(state.ss, state.se); setSelectionStartEnd(state.ss, state.se);
$inputElt.trigger('input'); $inputElt.trigger('input');
}; };
@ -740,7 +706,7 @@ define([
clearNewline = true; clearNewline = true;
} }
currentMode = 'newlines'; undoManager.currentMode = 'newlines';
state.before += '\n' + indent; state.before += '\n' + indent;
state.selection = ''; state.selection = '';
@ -750,7 +716,6 @@ define([
}; };
}; };
var sectionList = []; var sectionList = [];
var sectionsToRemove = []; var sectionsToRemove = [];
var modifiedSections = []; var modifiedSections = [];
@ -822,11 +787,11 @@ define([
highlight(section); highlight(section);
newSectionEltList.appendChild(section.elt); newSectionEltList.appendChild(section.elt);
}); });
noWatch(function() { watcher.noWatch(function() {
if(fileChanged === true) { if(fileChanged === true) {
editor.contentElt.innerHTML = ''; editor.contentElt.innerHTML = '';
editor.contentElt.appendChild(newSectionEltList); editor.contentElt.appendChild(newSectionEltList);
inputElt.setSelectionStartEnd(selectionStart, selectionEnd); setSelectionStartEnd(selectionStart, selectionEnd);
} }
else { else {
// Remove outdated sections // Remove outdated sections
@ -852,7 +817,7 @@ define([
childNode = nextNode; childNode = nextNode;
} }
inputElt.setSelectionStartEnd(selectionStart, selectionEnd); setSelectionStartEnd(selectionStart, selectionEnd);
} }
}); });
} }

View File

@ -225,10 +225,8 @@ define([
var $previewContentsElt; var $previewContentsElt;
eventMgr.onAsyncPreview = function() { eventMgr.onAsyncPreview = function() {
logger.log("onAsyncPreview"); logger.log("onAsyncPreview");
logger.log("Conversion time: " + (new Date() - eventMgr.previewStartTime));
function recursiveCall(callbackList) { function recursiveCall(callbackList) {
var callback = callbackList.length ? callbackList.shift() : function() { var callback = callbackList.length ? callbackList.shift() : function() {
logger.log("Preview time: " + (new Date() - eventMgr.previewStartTime));
_.defer(function() { _.defer(function() {
var html = ""; var html = "";
_.each(previewContentsElt.children, function(elt) { _.each(previewContentsElt.children, function(elt) {

View File

@ -35,8 +35,24 @@ define([
selectedFileDesc = fileDesc; selectedFileDesc = fileDesc;
}; };
var textareaElt; var htmlWithComments, htmlWithoutComments;
buttonHtmlCode.onPreviewFinished = function(htmlWithComments, htmlWithoutComments) { buttonHtmlCode.onPreviewFinished = function(htmlWithCommentsParam, htmlWithoutCommentsParam) {
htmlWithComments = htmlWithCommentsParam;
htmlWithoutComments = htmlWithoutCommentsParam;
};
buttonHtmlCode.onReady = function() {
var textareaElt = document.getElementById('input-html-code');
$(".action-html-code").click(function() {
_.defer(function() {
$("#input-html-code").each(function() {
if($(this).is(":hidden")) {
return;
}
this.select();
});
});
}).parent().on('show.bs.dropdown', function() {
try { try {
var htmlCode = _.template(buttonHtmlCode.config.template, { var htmlCode = _.template(buttonHtmlCode.config.template, {
documentTitle: selectedFileDesc.title, documentTitle: selectedFileDesc.title,
@ -51,21 +67,7 @@ define([
} }
catch(e) { catch(e) {
eventMgr.onError(e); eventMgr.onError(e);
return e.message;
} }
};
buttonHtmlCode.onReady = function() {
textareaElt = document.getElementById('input-html-code');
$(".action-html-code").click(function() {
_.defer(function() {
$("#input-html-code").each(function() {
if($(this).is(":hidden")) {
return;
}
this.select();
});
});
}); });
}; };

View File

@ -20,7 +20,7 @@ define([
].join(''); ].join('');
var popoverTitleTmpl = [ var popoverTitleTmpl = [
'<span class="clearfix">', '<span class="clearfix">',
' <a href="#" class="action-remove-discussion pull-right">', ' <a href="#" class="action-remove-discussion pull-right<%= !type ? \'\': \' hide\' %>">',
' <i class="icon-trash"></i>', ' <i class="icon-trash"></i>',
' </a>', ' </a>',
' <%- title %>', ' <%- title %>',
@ -35,7 +35,11 @@ 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 top = (y - 8) + 'px'; var yOffset = -8;
if(commentElt.className.indexOf(' icon-fork') !== -1) {
yOffset = -12;
}
var top = (y + yOffset) + 'px';
var right = ((offsetMap[lineIndex] || 0) * 25 + 10) + 'px'; var right = ((offsetMap[lineIndex] || 0) * 25 + 10) + 'px';
commentElt.style.top = top; commentElt.style.top = top;
commentElt.style.right = right; commentElt.style.right = right;
@ -46,7 +50,7 @@ define([
var marginElt; var marginElt;
var commentEltList = []; var commentEltList = [];
var newCommentElt = crel('a', { var newCommentElt = crel('a', {
class: 'icon-comment new' class: 'discussion icon-comment new'
}); });
var cursorY; var cursorY;
comments.onCursorCoordinates = function(x, y) { comments.onCursorCoordinates = function(x, y) {
@ -69,6 +73,7 @@ define([
var cssApplier; var cssApplier;
var currentFileDesc; var currentFileDesc;
var refreshTimeoutId;
var refreshDiscussions = _.debounce(function() { var refreshDiscussions = _.debounce(function() {
if(currentFileDesc === undefined) { if(currentFileDesc === undefined) {
return; return;
@ -80,11 +85,28 @@ define([
}); });
commentEltList = []; commentEltList = [];
offsetMap = {}; offsetMap = {};
_.each(currentFileDesc.discussionList, function(discussion) { var discussionList = _.values(currentFileDesc.discussionList);
var isReplied = _.last(discussion.commentList).author != author; function refreshOne() {
if(discussionList.length === 0) {
// Move newCommentElt
setCommentEltCoordinates(newCommentElt, cursorY);
if(currentContext && !currentContext.discussion.discussionIndex) {
inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
movePopover(newCommentElt);
}
return;
}
var discussion = discussionList.pop();
var commentElt = crel('a', { var commentElt = crel('a', {
class: 'icon-comment' + (isReplied ? ' replied' : ' added') class: 'discussion'
}); });
if(discussion.type == 'conflict') {
commentElt.className += ' icon-fork';
}
else {
var isReplied = _.last(discussion.commentList).author != author;
commentElt.className += ' icon-comment' + (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);
@ -96,14 +118,10 @@ define([
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);
} }
}); refreshTimeoutId = setTimeout(refreshOne, 5);
// Move newCommentElt
setCommentEltCoordinates(newCommentElt, cursorY);
if(currentContext && !currentContext.discussion.discussionIndex) {
inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
movePopover(newCommentElt);
} }
clearTimeout(refreshTimeoutId);
refreshTimeoutId = setTimeout(refreshOne, 5);
}, 50); }, 50);
comments.onFileOpen = function(fileDesc) { comments.onFileOpen = function(fileDesc) {
@ -155,7 +173,7 @@ define([
comments.onDiscussionRemoved = function(fileDesc, discussion) { comments.onDiscussionRemoved = function(fileDesc, discussion) {
if(currentFileDesc === fileDesc) { if(currentFileDesc === fileDesc) {
// Close popover if the discussion has removed // Close popover if the discussion has been removed
if(currentContext !== undefined && currentContext.discussion.discussionIndex == discussion.discussionIndex) { if(currentContext !== undefined && currentContext.discussion.discussionIndex == discussion.discussionIndex) {
closeCurrentPopover(); closeCurrentPopover();
} }
@ -168,6 +186,9 @@ define([
}; };
function getDiscussionComments() { function getDiscussionComments() {
if(currentContext.discussion.type == 'conflict') {
return '';
}
return currentContext.discussion.commentList.map(function(comment) { return currentContext.discussion.commentList.map(function(comment) {
return _.template(commentTmpl, { return _.template(commentTmpl, {
author: comment.author || 'Anonymous', author: comment.author || 'Anonymous',
@ -206,16 +227,18 @@ define([
title += '...'; title += '...';
} }
return _.template(popoverTitleTmpl, { return _.template(popoverTitleTmpl, {
title: title title: title,
type: currentContext.discussion.type
}); });
}, },
content: function() { content: function() {
var content = _.template(commentsPopoverContentHTML, { var content = _.template(commentsPopoverContentHTML, {
commentList: getDiscussionComments() commentList: getDiscussionComments(),
type: currentContext.discussion.type
}); });
return content; return content;
}, },
selector: '#wmd-input > .editor-margin > .icon-comment' 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 = {
@ -299,7 +322,7 @@ define([
// Create discussion index // Create discussion index
var discussionIndex; var discussionIndex;
do { do {
discussionIndex = utils.randomString(); discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
} while(_.has(discussionList, discussionIndex)); } while(_.has(discussionList, discussionIndex));
context.discussion.discussionIndex = discussionIndex; context.discussion.discussionIndex = discussionIndex;
discussionList[discussionIndex] = context.discussion; discussionList[discussionIndex] = context.discussion;
@ -316,8 +339,14 @@ define([
var $removeButton = $(context.popoverElt.querySelector('.action-remove-discussion')); var $removeButton = $(context.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 = $(context.popoverElt.querySelector('.action-remove-discussion-cancel')); /*
var $removeConfirmButton = $(context.popoverElt.querySelector('.action-remove-discussion-confirm')); 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'));
$removeButton.click(function() { $removeButton.click(function() {
$(context.popoverElt.querySelector('.new-comment-block')).addClass('hide'); $(context.popoverElt.querySelector('.new-comment-block')).addClass('hide');
$(context.popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide'); $(context.popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide');

View File

@ -2,8 +2,10 @@ define([
"underscore", "underscore",
"extensions/markdownExtra", "extensions/markdownExtra",
"extensions/mathJax", "extensions/mathJax",
"extensions/partialRendering",
"classes/Extension", "classes/Extension",
], function(_, markdownExtra, mathJax, Extension) { "crel",
], function(_, markdownExtra, mathJax, partialRendering, Extension, crel) {
var markdownSectionParser = new Extension("markdownSectionParser", "Markdown section parser"); var markdownSectionParser = new Extension("markdownSectionParser", "Markdown section parser");
@ -13,6 +15,7 @@ define([
}; };
var sectionList = []; var sectionList = [];
var previewContentsElt;
// Regexp to look for section delimiters // Regexp to look for section delimiters
var regexp = '^.+[ \\t]*\\n=+[ \\t]*\\n+|^.+[ \\t]*\\n-+[ \\t]*\\n+|^\\#{1,6}[ \\t]*.+?[ \\t]*\\#*\\n+'; // Title delimiters var regexp = '^.+[ \\t]*\\n=+[ \\t]*\\n+|^.+[ \\t]*\\n-+[ \\t]*\\n+|^\\#{1,6}[ \\t]*.+?[ \\t]*\\#*\\n+'; // Title delimiters
@ -33,11 +36,48 @@ define([
regexp = new RegExp(regexp, 'gm'); regexp = new RegExp(regexp, 'gm');
var converter = editor.getConverter(); var converter = editor.getConverter();
if(!partialRendering.enabled) {
converter.hooks.chain("preConversion", function() { converter.hooks.chain("preConversion", function() {
return _.reduce(sectionList, function(result, section) { return _.reduce(sectionList, function(result, section) {
return result + section.previewText; return result + '\n<div class="se-preview-section-delimiter"></div>\n\n' + section.text + '\n\n';
}, ''); }, '');
}); });
editor.hooks.chain("onPreviewRefresh", function() {
var wmdPreviewElt = document.getElementById("wmd-preview");
var childNode = wmdPreviewElt.firstChild;
function createSectionElt() {
var sectionElt = crel('div', {
class: 'wmd-preview-section preview-content'
});
var isNextDelimiter = false;
while (childNode) {
var nextNode = childNode.nextSibling;
var isDelimiter = childNode.className == 'se-preview-section-delimiter';
if(isNextDelimiter === true && childNode.tagName == 'DIV' && isDelimiter) {
// Stop when encountered the next delimiter
break;
}
isNextDelimiter = true;
isDelimiter || sectionElt.appendChild(childNode);
childNode = nextNode;
}
return sectionElt;
}
var newSectionEltList = document.createDocumentFragment();
sectionList.forEach(function(section) {
newSectionEltList.appendChild(createSectionElt(section));
});
previewContentsElt.innerHTML = '';
previewContentsElt.appendChild(wmdPreviewElt);
previewContentsElt.appendChild(newSectionEltList);
});
}
};
markdownSectionParser.onReady = function() {
previewContentsElt = document.getElementById("preview-contents");
}; };
var fileDesc; var fileDesc;

View File

@ -1,5 +1,5 @@
<div class="discussion-comment-list"><%= commentList %></div> <div class="discussion-comment-list"><%= commentList %></div>
<div class="new-comment-block"> <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> <input class="form-control input-comment-author" placeholder="Your name"></input>
<textarea class="form-control input-comment-content"></textarea> <textarea class="form-control input-comment-content"></textarea>
@ -8,6 +8,12 @@
<button class="btn btn-primary action-add-comment">Add</button> <button class="btn btn-primary action-add-comment">Add</button>
</div> </div>
</div> </div>
<div class="conflict-review<%= type == 'conflict' ? '': ' hide' %>">
<p>Multiple users have made conflicting modifications that you have to review.</p>
<div class="form-group text-right">
<button class="btn btn-primary action-remove-discussion-confirm">Mark as resolved</button>
</div>
</div>
<div class="remove-discussion-confirm hide"> <div class="remove-discussion-confirm hide">
<br/> <br/>
<blockquote>Remove this discussion, really?</blockquote> <blockquote>Remove this discussion, really?</blockquote>

View File

@ -56,8 +56,8 @@ define([
var syncAttributes = createSyncAttributes(file.path, file.versionTag, file.content); var syncAttributes = createSyncAttributes(file.path, file.versionTag, file.content);
var syncLocations = {}; var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes; syncLocations[syncAttributes.syncIndex] = syncAttributes;
var parsingResult = dropboxProvider.parseSerializedContent(file.content); var parsedContent = dropboxProvider.parseSerializedContent(file.content);
var fileDesc = fileMgr.createFile(file.name, parsingResult.content, parsingResult.discussionList, syncLocations); var fileDesc = fileMgr.createFile(file.name, parsedContent.content, parsedContent.discussionList, syncLocations);
fileMgr.selectFile(fileDesc); fileMgr.selectFile(fileDesc);
fileDescList.push(fileDesc); fileDescList.push(fileDesc);
}); });
@ -165,7 +165,12 @@ define([
callback(error); callback(error);
return; return;
} }
_.each(changes, function(change) { function merge() {
if(changes.length === 0) {
storage[PROVIDER_DROPBOX + ".lastChangeId"] = newChangeId;
return callback();
}
var change = changes.pop();
var syncAttributes = change.syncAttributes; var syncAttributes = change.syncAttributes;
var syncIndex = syncAttributes.syncIndex; var syncIndex = syncAttributes.syncIndex;
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
@ -173,48 +178,30 @@ define([
if(fileDesc === undefined) { if(fileDesc === undefined) {
return; return;
} }
var localTitle = fileDesc.title;
// File deleted // File deleted
if(change.wasRemoved === true) { if(change.wasRemoved === true) {
eventMgr.onError('"' + localTitle + '" has been removed from Dropbox.'); eventMgr.onError('"' + fileDesc.title + '" has been removed from Dropbox.');
fileDesc.removeSyncLocation(syncAttributes); fileDesc.removeSyncLocation(syncAttributes);
return eventMgr.onSyncRemoved(fileDesc, syncAttributes); return eventMgr.onSyncRemoved(fileDesc, syncAttributes);
} }
var localContent = fileDesc.content;
var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent);
var localDiscussionList = fileDesc.discussionListJSON;
var localDiscussionListChanged = syncAttributes.discussionListCRC != utils.crc32(localDiscussionList);
var file = change.stat; var file = change.stat;
var parsingResult = dropboxProvider.parseSerializedContent(file.content); var parsedContent = dropboxProvider.parseSerializedContent(file.content);
var remoteContent = parsingResult.content; var remoteContent = parsedContent.content;
var remoteDiscussionList = parsingResult.discussionList; var remoteDiscussionList = parsedContent.discussionList;
var remoteContentCRC = utils.crc32(remoteContent); var remoteCRC = dropboxProvider.syncMerge(fileDesc, syncAttributes, remoteContent, fileDesc.title, remoteDiscussionList);
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionList);
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
var fileContentChanged = localContent != remoteContent;
var fileDiscussionListChanged = localDiscussionList != remoteDiscussionList;
// Conflict detection
if(fileContentChanged === true && localContentChanged === true && remoteContentChanged === true) {
fileMgr.createFile(localTitle + " (backup)", localContent);
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
}
// If file content changed
if(fileContentChanged && remoteContentChanged === true) {
fileDesc.content = file.content;
eventMgr.onContentChanged(fileDesc, file.content);
eventMgr.onMessage('"' + localTitle + '" has been updated from Dropbox.');
if(fileMgr.currentFile === fileDesc) {
fileMgr.selectFile(); // Refresh editor
}
}
// Update syncAttributes // Update syncAttributes
syncAttributes.version = file.versionTag; syncAttributes.version = file.versionTag;
syncAttributes.contentCRC = remoteContentCRC; if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = remoteContent;
syncAttributes.discussionList = remoteDiscussionList;
}
syncAttributes.contentCRC = remoteCRC.contentCRC;
syncAttributes.discussionListCRC = remoteCRC.discussionListCRC;
utils.storeAttributes(syncAttributes); utils.storeAttributes(syncAttributes);
}); setTimeout(merge, 5);
storage[PROVIDER_DROPBOX + ".lastChangeId"] = newChangeId; }
callback(); setTimeout(merge, 5);
}); });
}); });
}; };

View File

@ -56,8 +56,8 @@ define([
syncAttributes.isRealtime = file.isRealtime; syncAttributes.isRealtime = file.isRealtime;
var syncLocations = {}; var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes; syncLocations[syncAttributes.syncIndex] = syncAttributes;
var parsingResult = gdriveProvider.parseSerializedContent(file.content); var parsedContent = gdriveProvider.parseSerializedContent(file.content);
fileDesc = fileMgr.createFile(file.title, parsingResult.content, parsingResult.discussionList, syncLocations); fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionList, syncLocations);
fileDescList.push(fileDesc); fileDescList.push(fileDesc);
}); });
if(fileDesc !== undefined) { if(fileDesc !== undefined) {
@ -200,19 +200,22 @@ define([
callback(error); callback(error);
return; return;
} }
_.each(changes, function(change) { function merge() {
if(changes.length === 0) {
storage[accountId + ".gdrive.lastChangeId"] = newChangeId;
return callback();
}
var change = changes.pop();
var syncAttributes = change.syncAttributes; var syncAttributes = change.syncAttributes;
var syncIndex = syncAttributes.syncIndex; var syncIndex = syncAttributes.syncIndex;
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex); var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
// No file corresponding (file may have been deleted // No file corresponding (file may have been deleted locally)
// locally)
if(fileDesc === undefined) { if(fileDesc === undefined) {
return; return;
} }
var localTitle = fileDesc.title;
// File deleted // File deleted
if(change.deleted === true) { if(change.deleted === true) {
eventMgr.onError('"' + localTitle + '" has been removed from ' + providerName + '.'); eventMgr.onError('"' + fileDesc.title + '" has been removed from ' + providerName + '.');
fileDesc.removeSyncLocation(syncAttributes); fileDesc.removeSyncLocation(syncAttributes);
eventMgr.onSyncRemoved(fileDesc, syncAttributes); eventMgr.onSyncRemoved(fileDesc, syncAttributes);
if(syncAttributes.isRealtime === true && fileMgr.currentFile === fileDesc) { if(syncAttributes.isRealtime === true && fileMgr.currentFile === fileDesc) {
@ -220,46 +223,28 @@ define([
} }
return; return;
} }
var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle);
var localContent = fileDesc.content;
var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent);
var file = change.file; var file = change.file;
var remoteTitleCRC = utils.crc32(file.title); var parsedContent = gdriveProvider.parseSerializedContent(file.content);
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC; var remoteContent = parsedContent.content;
var fileTitleChanged = localTitle != file.title; var remoteTitle = file.title;
var remoteContentCRC = utils.crc32(file.content); var remoteDiscussionList = parsedContent.discussionList;
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList);
var fileContentChanged = localContent != file.content;
// Conflict detection
if((fileTitleChanged === true && localTitleChanged === true && remoteTitleChanged === true) || (!syncAttributes.isRealtime && fileContentChanged === true && localContentChanged === true && remoteContentChanged === true)) {
fileMgr.createFile(localTitle + " (backup)", localContent);
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
}
// If file title changed
if(fileTitleChanged && remoteTitleChanged === true) {
fileDesc.title = file.title;
eventMgr.onTitleChanged(fileDesc);
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + file.title + '" on ' + providerName + '.');
}
// If file content changed
if(!syncAttributes.isRealtime && fileContentChanged && remoteContentChanged === true) {
fileDesc.content = file.content;
eventMgr.onContentChanged(fileDesc, file.content);
eventMgr.onMessage('"' + file.title + '" has been updated from ' + providerName + '.');
if(fileMgr.currentFile === fileDesc) {
fileMgr.selectFile(); // Refresh editor
}
}
// Update syncAttributes // Update syncAttributes
syncAttributes.etag = file.etag; syncAttributes.etag = file.etag;
if(!syncAttributes.isRealtime) { if(merge === true) {
syncAttributes.contentCRC = remoteContentCRC; // Need to store the whole content for merge
syncAttributes.content = remoteContent;
syncAttributes.title = remoteTitle;
syncAttributes.discussionList = remoteDiscussionList;
} }
syncAttributes.titleCRC = remoteTitleCRC; syncAttributes.contentCRC = remoteCRC.contentCRC;
syncAttributes.titleCRC = remoteCRC.titleCRC;
syncAttributes.discussionListCRC = remoteCRC.discussionListCRC;
utils.storeAttributes(syncAttributes); utils.storeAttributes(syncAttributes);
}); setTimeout(merge, 5);
storage[accountId + ".gdrive.lastChangeId"] = newChangeId; }
callback(); setTimeout(merge, 5);
}); });
}); });
}; };

View File

@ -151,7 +151,7 @@ h1, h2, h3, h4, h5, h6 {
} }
pre { pre {
word-break: break-all; word-break: break-word;
} }
p, p,

View File

@ -22,7 +22,7 @@
@secondary-bg: lighten(@secondary-desaturated, 45%); @secondary-bg: lighten(@secondary-desaturated, 45%);
@secondary-bg-light: lighten(@secondary-desaturated, 47%); @secondary-bg-light: lighten(@secondary-desaturated, 47%);
@secondary-bg-lighter: #fff; @secondary-bg-lighter: #fff;
@secondary-color: @primary-desaturated; @secondary-color: lighten(@primary-desaturated, 10%);
@secondary-color-dark: darken(@secondary-color, 12.5%); @secondary-color-dark: darken(@secondary-color, 12.5%);
@secondary-color-darker: darken(@secondary-color, 25%); @secondary-color-darker: darken(@secondary-color, 25%);
@secondary-color-darkest: darken(@secondary-color, 37.5%); @secondary-color-darkest: darken(@secondary-color, 37.5%);
@ -34,7 +34,7 @@
@tertiary-bg: #fff; @tertiary-bg: #fff;
@tertiary-color-lighter: fade(@tertiary-color, 40%); @tertiary-color-lighter: fade(@tertiary-color, 40%);
@tertiary-color-light: fade(@tertiary-color, 60%); @tertiary-color-light: fade(@tertiary-color, 60%);
@tertiary-color: darken(@secondary-desaturated, 7.5%); @tertiary-color: darken(@secondary-desaturated, 5%);
@tertiary-color-dark: darken(@tertiary-color, 15%); @tertiary-color-dark: darken(@tertiary-color, 15%);
@tertiary-color-darker: darken(@tertiary-color, 30%); @tertiary-color-darker: darken(@tertiary-color, 30%);
@ -1050,30 +1050,41 @@ a {
background-color: @tertiary-bg; background-color: @tertiary-bg;
overflow: auto; overflow: auto;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-wrap: break-word;
> .editor-content { > .editor-content {
padding-bottom: 230px; padding-bottom: 230px;
} }
> .editor-margin { > .editor-margin {
position: absolute; position: absolute;
top: 0; top: 0;
.icon-comment { .discussion {
font-size: 18px;
&.new { &.new {
color: fade(@tertiary-color, 10%); color: fade(@tertiary-color, 10%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: fade(@tertiary-color, 35%) !important; color: fade(@tertiary-color, 35%) !important;
} }
} }
&.added {
color: fade(@label-warning-bg, 45%);
&:hover, &.active, &.active:hover {
color: fade(@label-warning-bg, 80%) !important;
}
}
&.replied { &.replied {
color: fade(@label-danger-bg, 45%); color: fade(@label-danger-bg, 45%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: fade(@label-danger-bg, 80%) !important; color: fade(@label-danger-bg, 80%) !important;
} }
} }
&.added { &.icon-fork {
color: fade(@label-warning-bg, 45%); font-size: 22px;
color: fade(@label-danger-bg, 70%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: fade(@label-warning-bg, 80%) !important; color: @label-danger-bg !important;
}
&:before {
margin-right: 0;
} }
} }
position: absolute; position: absolute;

View File

@ -78,8 +78,7 @@ define([
// No more synchronized location for this document // No more synchronized location for this document
if(uploadSyncAttributesList.length === 0) { if(uploadSyncAttributesList.length === 0) {
fileUp(callback); return fileUp(callback);
return;
} }
// Dequeue a synchronized location // Dequeue a synchronized location
@ -98,16 +97,15 @@ define([
uploadTitle, uploadTitle,
uploadTitleCRC, uploadTitleCRC,
uploadDiscussionList, uploadDiscussionList,
uploadTitleCRC,
uploadDiscussionListCRC, uploadDiscussionListCRC,
syncAttributes,
function(error, uploadFlag) { function(error, uploadFlag) {
if(uploadFlag === true) { if(uploadFlag === true) {
// If uploadFlag is true, request another upload cycle // If uploadFlag is true, request another upload cycle
uploadCycle = true; uploadCycle = true;
} }
if(error) { if(error) {
callback(error); return callback(error);
return;
} }
if(uploadFlag) { if(uploadFlag) {
// Update syncAttributes in storage // Update syncAttributes in storage
@ -124,16 +122,14 @@ define([
// No more fileDesc to synchronize // No more fileDesc to synchronize
if(uploadFileList.length === 0) { if(uploadFileList.length === 0) {
syncUp(callback); return syncUp(callback);
return;
} }
// Dequeue a fileDesc to synchronize // Dequeue a fileDesc to synchronize
var fileDesc = uploadFileList.pop(); var fileDesc = uploadFileList.pop();
uploadSyncAttributesList = _.values(fileDesc.syncLocations); uploadSyncAttributesList = _.values(fileDesc.syncLocations);
if(uploadSyncAttributesList.length === 0) { if(uploadSyncAttributesList.length === 0) {
fileUp(callback); return fileUp(callback);
return;
} }
// Get document title/content // Get document title/content
@ -164,22 +160,19 @@ define([
var providerList = []; var providerList = [];
function providerDown(callback) { function providerDown(callback) {
if(providerList.length === 0) { if(providerList.length === 0) {
callback(); return callback();
return;
} }
var provider = providerList.pop(); var provider = providerList.pop();
// Check that provider has files to sync // Check that provider has files to sync
if(!synchronizer.hasSync(provider)) { if(!synchronizer.hasSync(provider)) {
providerDown(callback); return providerDown(callback);
return;
} }
// Perform provider's syncDown // Perform provider's syncDown
provider.syncDown(function(error) { provider.syncDown(function(error) {
if(error) { if(error) {
callback(error); return callback(error);
return;
} }
providerDown(callback); providerDown(callback);
}); });
@ -354,8 +347,7 @@ define([
if(isRealtime) { if(isRealtime) {
if(_.size(fileDesc.syncLocations) > 0) { if(_.size(fileDesc.syncLocations) > 0) {
eventMgr.onError("Real time collaborative document can't be synchronized with multiple locations"); return eventMgr.onError("Real time collaborative document can't be synchronized with multiple locations");
return;
} }
// Perform the provider's real time export // Perform the provider's real time export
provider.exportRealtimeFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) { provider.exportRealtimeFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) {