Fixed sync merge
This commit is contained in:
parent
acebad8a65
commit
4ae6e540d4
@ -4,9 +4,10 @@ define([
|
||||
'settings',
|
||||
'eventMgr',
|
||||
'fileMgr',
|
||||
'editor',
|
||||
'diff_match_patch_uncompressed',
|
||||
'jsondiffpatch',
|
||||
], function(_, utils, settings, eventMgr, fileMgr, diff_match_patch, jsondiffpatch) {
|
||||
], function(_, utils, settings, eventMgr, fileMgr, editor, diff_match_patch, jsondiffpatch) {
|
||||
|
||||
function Provider(providerId, providerName) {
|
||||
this.providerId = providerId;
|
||||
@ -26,6 +27,9 @@ define([
|
||||
) {
|
||||
throw 'invalid';
|
||||
}
|
||||
if(discussion.type == 'conflict') {
|
||||
return;
|
||||
}
|
||||
discussion.commentList.forEach(function(comment) {
|
||||
if(
|
||||
(!_.isString(comment.author)) ||
|
||||
@ -42,7 +46,7 @@ define([
|
||||
};
|
||||
|
||||
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;
|
||||
@ -62,6 +66,8 @@ define([
|
||||
};
|
||||
|
||||
var diffMatchPatch = new diff_match_patch();
|
||||
diffMatchPatch.Match_Threshold = 0;
|
||||
diffMatchPatch.Patch_DeleteThreshold = 0;
|
||||
var jsonDiffPatch = jsondiffpatch.create({
|
||||
objectHash: function(obj) {
|
||||
return JSON.stringify(obj);
|
||||
@ -96,7 +102,7 @@ define([
|
||||
|
||||
function moveComments(oldTextContent, newTextContent, discussionList) {
|
||||
var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent);
|
||||
var updateDiscussionList = false;
|
||||
var changed = false;
|
||||
var startOffset = 0;
|
||||
changes.forEach(function(change) {
|
||||
var changeType = change[0];
|
||||
@ -115,30 +121,32 @@ define([
|
||||
// selectionEnd
|
||||
if(discussion.selectionEnd >= endOffset) {
|
||||
discussion.selectionEnd += diffOffset;
|
||||
updateDiscussionList = true;
|
||||
changed = true;
|
||||
}
|
||||
else if(discussion.selectionEnd > startOffset) {
|
||||
discussion.selectionEnd = startOffset;
|
||||
updateDiscussionList = true;
|
||||
changed = true;
|
||||
}
|
||||
// selectionStart
|
||||
if(discussion.selectionStart >= endOffset) {
|
||||
discussion.selectionStart += diffOffset;
|
||||
updateDiscussionList = true;
|
||||
changed = true;
|
||||
}
|
||||
else if(discussion.selectionStart > startOffset) {
|
||||
discussion.selectionStart = startOffset;
|
||||
updateDiscussionList = true;
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
startOffset = endOffset;
|
||||
});
|
||||
return updateDiscussionList;
|
||||
return changed;
|
||||
}
|
||||
|
||||
var localContent = fileDesc.content;
|
||||
var localTitle = fileDesc.title;
|
||||
var localDiscussionListJSON = fileDesc.discussionListJSON;
|
||||
var localDiscussionList = fileDesc.discussionList;
|
||||
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
|
||||
|
||||
// Local/Remote CRCs
|
||||
var localContentCRC = utils.crc32(localContent);
|
||||
@ -153,6 +161,7 @@ define([
|
||||
var localContentChanged = syncAttributes.contentCRC != localContentCRC;
|
||||
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
|
||||
var contentConflict = contentChanged && localContentChanged && remoteContentChanged;
|
||||
contentChanged = contentChanged && remoteContentChanged;
|
||||
|
||||
// Check title
|
||||
syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox
|
||||
@ -160,14 +169,16 @@ define([
|
||||
var localTitleChanged = syncAttributes.titleCRC != localTitleCRC;
|
||||
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
|
||||
var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged;
|
||||
titleChanged = titleChanged && remoteTitleChanged;
|
||||
|
||||
// Check discussionList
|
||||
var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON;
|
||||
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
|
||||
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
|
||||
var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged;
|
||||
discussionListChanged = discussionListChanged && remoteDiscussionListChanged;
|
||||
|
||||
// Conflict detection
|
||||
var conflictList = [];
|
||||
if(
|
||||
(!merge && (contentConflict || titleConflict || discussionListConflict)) ||
|
||||
(contentConflict && syncAttributes.content === undefined) ||
|
||||
@ -178,28 +189,61 @@ define([
|
||||
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
|
||||
}
|
||||
else {
|
||||
var updateDiscussionList = remoteDiscussionListChanged;
|
||||
var localDiscussionList = fileDesc.discussionList;
|
||||
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
|
||||
var oldDiscussionList;
|
||||
var patch, delta;
|
||||
if(contentConflict) {
|
||||
// Patch content (line mode)
|
||||
var oldContent = syncAttributes.content;
|
||||
/*
|
||||
var oldContentLines = linesToChars(syncAttributes.content);
|
||||
var localContentLines = linesToChars(localContent);
|
||||
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];
|
||||
var newContent = remoteContentLines.split('').map(function(char) {
|
||||
return lineArray[char.charCodeAt(0)];
|
||||
}).join('\n');
|
||||
*/
|
||||
|
||||
// Whether we take the local discussionList into account
|
||||
if(localDiscussionListChanged || !remoteDiscussionListChanged) {
|
||||
// Move local discussion according to content patch
|
||||
var localDiscussionArray = _.values(localDiscussionList);
|
||||
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
|
||||
updateDiscussionList |= moveComments(localContent, newContent, localDiscussionArray);
|
||||
discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray);
|
||||
}
|
||||
|
||||
if(remoteDiscussionListChanged) {
|
||||
@ -217,6 +261,21 @@ define([
|
||||
else {
|
||||
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;
|
||||
}
|
||||
else if(discussionListConflict) {
|
||||
@ -232,21 +291,57 @@ define([
|
||||
}
|
||||
}
|
||||
|
||||
if(titleChanged && remoteTitleChanged) {
|
||||
if(titleChanged) {
|
||||
fileDesc.title = remoteTitle;
|
||||
eventMgr.onTitleChanged(fileDesc);
|
||||
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.');
|
||||
}
|
||||
if(contentChanged && remoteContentChanged === true) {
|
||||
if(fileMgr.currentFile === fileDesc) {
|
||||
document.getElementById('wmd-input').setValueSilently(remoteContent);
|
||||
}
|
||||
else {
|
||||
fileDesc.content = remoteContent;
|
||||
eventMgr.onContentChanged(fileDesc, remoteContent);
|
||||
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + this.providerName + '.');
|
||||
}
|
||||
|
||||
if(contentChanged || discussionListChanged) {
|
||||
var self = this;
|
||||
editor.watcher.noWatch(function() {
|
||||
if(contentChanged) {
|
||||
if(!/\n$/.test(remoteContent)) {
|
||||
remoteContent += '\n';
|
||||
}
|
||||
if(fileMgr.currentFile === fileDesc) {
|
||||
editor.setValueNoWatch(remoteContent);
|
||||
}
|
||||
fileDesc.content = remoteContent;
|
||||
eventMgr.onContentChanged(fileDesc, remoteContent);
|
||||
}
|
||||
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;
|
||||
|
@ -63,23 +63,76 @@ define([
|
||||
fileDesc = selectedFileDesc;
|
||||
});
|
||||
|
||||
var contentObserver;
|
||||
var isWatching = false;
|
||||
function noWatch(cb) {
|
||||
if(isWatching === true) {
|
||||
contentObserver.disconnect();
|
||||
isWatching = false;
|
||||
cb();
|
||||
isWatching = true;
|
||||
// Watcher used to detect editor changes
|
||||
function Watcher() {
|
||||
this.isWatching = false;
|
||||
var contentObserver;
|
||||
this.startWatching = function() {
|
||||
this.isWatching = true;
|
||||
contentObserver = contentObserver || new MutationObserver(checkContentChange);
|
||||
contentObserver.observe(editor.contentElt, {
|
||||
childList: true,
|
||||
subtree: 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 {
|
||||
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);
|
||||
}
|
||||
else {
|
||||
cb();
|
||||
}
|
||||
range.setEnd(offset.element, offset.offset);
|
||||
return range;
|
||||
}
|
||||
|
||||
var diffMatchPatch = new diff_match_patch();
|
||||
@ -96,11 +149,7 @@ define([
|
||||
});
|
||||
|
||||
var previousTextContent;
|
||||
var currentMode;
|
||||
editor.undoManager = (function() {
|
||||
var undoManager = {
|
||||
onButtonStateChange: function() {}
|
||||
};
|
||||
function UndoManager() {
|
||||
var undoStack = [];
|
||||
var redoStack = [];
|
||||
var lastTime;
|
||||
@ -108,14 +157,15 @@ define([
|
||||
var currentState;
|
||||
var selectionStartBefore;
|
||||
var selectionEndBefore;
|
||||
undoManager.setCommandMode = function() {
|
||||
currentMode = 'command';
|
||||
this.setCommandMode = function() {
|
||||
this.currentMode = 'command';
|
||||
};
|
||||
undoManager.setMode = function() {}; // For compatibility with PageDown
|
||||
undoManager.saveState = function() {
|
||||
this.setMode = function() {}; // For compatibility with PageDown
|
||||
this.onButtonStateChange = function() {}; // To be overridden by PageDown
|
||||
this.saveState = function() {
|
||||
redoStack = [];
|
||||
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);
|
||||
// Limit the size of the stack
|
||||
if(undoStack.length === 100) {
|
||||
@ -135,32 +185,34 @@ define([
|
||||
discussionListJSON: fileDesc.discussionListJSON
|
||||
};
|
||||
lastTime = currentTime;
|
||||
lastMode = currentMode;
|
||||
currentMode = undefined;
|
||||
undoManager.onButtonStateChange();
|
||||
lastMode = this.currentMode;
|
||||
this.currentMode = undefined;
|
||||
this.onButtonStateChange();
|
||||
};
|
||||
undoManager.saveSelectionState = _.debounce(function() {
|
||||
if(currentMode === undefined) {
|
||||
this.saveSelectionState = _.debounce(function() {
|
||||
if(this.currentMode === undefined) {
|
||||
selectionStartBefore = selectionStart;
|
||||
selectionEndBefore = selectionEnd;
|
||||
}
|
||||
}, 10);
|
||||
undoManager.canUndo = function() {
|
||||
this.canUndo = function() {
|
||||
return undoStack.length;
|
||||
};
|
||||
undoManager.canRedo = function() {
|
||||
this.canRedo = function() {
|
||||
return redoStack.length;
|
||||
};
|
||||
var self = this;
|
||||
function restoreState(state, selectionStart, selectionEnd) {
|
||||
// Update editor
|
||||
noWatch(function() {
|
||||
watcher.noWatch(function() {
|
||||
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;
|
||||
if(discussionListJSON != state.discussionListJSON) {
|
||||
currentMode = 'undoredo'; // In order to avoid saveState
|
||||
var oldDiscussionList = fileDesc.discussionList;
|
||||
fileDesc.discussionListJSON = state.discussionListJSON;
|
||||
var newDiscussionList = fileDesc.discussionList;
|
||||
@ -184,12 +236,12 @@ define([
|
||||
selectionStartBefore = selectionStart;
|
||||
selectionEndBefore = selectionEnd;
|
||||
currentState = state;
|
||||
currentMode = undefined;
|
||||
self.currentMode = undefined;
|
||||
lastMode = undefined;
|
||||
undoManager.onButtonStateChange();
|
||||
self.onButtonStateChange();
|
||||
adjustCursorPosition();
|
||||
}
|
||||
undoManager.undo = function() {
|
||||
this.undo = function() {
|
||||
var state = undoStack.pop();
|
||||
if(!state) {
|
||||
return;
|
||||
@ -197,7 +249,7 @@ define([
|
||||
redoStack.push(currentState);
|
||||
restoreState(state, currentState.selectionStartBefore, currentState.selectionEndBefore);
|
||||
};
|
||||
undoManager.redo = function() {
|
||||
this.redo = function() {
|
||||
var state = redoStack.pop();
|
||||
if(!state) {
|
||||
return;
|
||||
@ -205,7 +257,7 @@ define([
|
||||
undoStack.push(currentState);
|
||||
restoreState(state, state.selectionStartAfter, state.selectionEndAfter);
|
||||
};
|
||||
undoManager.init = function() {
|
||||
this.init = function() {
|
||||
var content = fileDesc.content;
|
||||
undoStack = [];
|
||||
redoStack = [];
|
||||
@ -216,17 +268,18 @@ define([
|
||||
content: content,
|
||||
discussionListJSON: fileDesc.discussionListJSON
|
||||
};
|
||||
currentMode = undefined;
|
||||
this.currentMode = undefined;
|
||||
lastMode = undefined;
|
||||
editor.contentElt.textContent = content;
|
||||
};
|
||||
return undoManager;
|
||||
})();
|
||||
}
|
||||
var undoManager = new UndoManager();
|
||||
editor.undoManager = undoManager;
|
||||
|
||||
function onComment() {
|
||||
if(!currentMode) {
|
||||
currentMode = 'comment';
|
||||
editor.undoManager.saveState();
|
||||
if(watcher.isWatching === true) {
|
||||
undoManager.currentMode = 'comment';
|
||||
undoManager.saveState();
|
||||
}
|
||||
}
|
||||
eventMgr.addListener('onDiscussionCreated', onComment);
|
||||
@ -259,7 +312,7 @@ define([
|
||||
fileDesc.editorStart = selectionStart;
|
||||
fileDesc.editorEnd = selectionEnd;
|
||||
}
|
||||
editor.undoManager.saveSelectionState();
|
||||
undoManager.saveSelectionState();
|
||||
}
|
||||
|
||||
function checkContentChange() {
|
||||
@ -272,7 +325,7 @@ define([
|
||||
if(!/\n$/.test(currentTextContent)) {
|
||||
currentTextContent += '\n';
|
||||
}
|
||||
currentMode = currentMode || 'typing';
|
||||
undoManager.currentMode = undoManager.currentMode || 'typing';
|
||||
var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent);
|
||||
// Move comments according to changes
|
||||
var updateDiscussionList = false;
|
||||
@ -321,7 +374,7 @@ define([
|
||||
eventMgr.onContentChanged(fileDesc, currentTextContent);
|
||||
updateDiscussionList && eventMgr.onCommentsChanged(fileDesc);
|
||||
previousTextContent = currentTextContent;
|
||||
editor.undoManager.saveState();
|
||||
undoManager.saveState();
|
||||
}
|
||||
else {
|
||||
if(!/\n$/.test(currentTextContent)) {
|
||||
@ -339,56 +392,18 @@ define([
|
||||
}
|
||||
}
|
||||
|
||||
function findOffset(ss) {
|
||||
var offset = 0,
|
||||
element = editor.contentElt,
|
||||
container;
|
||||
|
||||
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 {
|
||||
element: element,
|
||||
offset: ss - offset
|
||||
};
|
||||
} else if (container) {
|
||||
element = container;
|
||||
|
||||
while (element && element.lastChild) {
|
||||
element = element.lastChild;
|
||||
}
|
||||
|
||||
if (element.nodeType === 3) {
|
||||
function findOffset(offset) {
|
||||
var walker = document.createTreeWalker(editor.contentElt, 4);
|
||||
while(walker.nextNode()) {
|
||||
var text = walker.currentNode.nodeValue || '';
|
||||
if (text.length > offset) {
|
||||
return {
|
||||
element: element,
|
||||
offset: element.textContent.length
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
element: element,
|
||||
offset: 0
|
||||
element: walker.currentNode,
|
||||
offset: offset
|
||||
};
|
||||
}
|
||||
offset -= text.length;
|
||||
}
|
||||
|
||||
return {
|
||||
element: editor.contentElt,
|
||||
offset: 0,
|
||||
@ -406,13 +421,13 @@ define([
|
||||
var selectedChar = inputElt.textContent[inputOffset];
|
||||
var selectionRange;
|
||||
if(selectedChar === undefined || selectedChar == '\n') {
|
||||
selectionRange = inputElt.createRange(inputOffset - 1, {
|
||||
selectionRange = createRange(inputOffset - 1, {
|
||||
element: element,
|
||||
offset: offset
|
||||
});
|
||||
}
|
||||
else {
|
||||
selectionRange = inputElt.createRange({
|
||||
selectionRange = createRange({
|
||||
element: element,
|
||||
offset: offset
|
||||
}, inputOffset + 1);
|
||||
@ -505,13 +520,7 @@ define([
|
||||
inputElt.appendChild(editor.marginElt);
|
||||
editor.$marginElt = $(editor.marginElt);
|
||||
|
||||
contentObserver = new MutationObserver(checkContentChange);
|
||||
isWatching = true;
|
||||
contentObserver.observe(editor.contentElt, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
watcher.startWatching();
|
||||
|
||||
$(inputElt).scroll(function() {
|
||||
scrollTop = inputElt.scrollTop;
|
||||
@ -527,7 +536,7 @@ define([
|
||||
|
||||
inputElt.focus = function() {
|
||||
editor.$contentElt.focus();
|
||||
this.setSelectionStartEnd(selectionStart, selectionEnd);
|
||||
setSelectionStartEnd(selectionStart, selectionEnd);
|
||||
inputElt.scrollTop = scrollTop;
|
||||
};
|
||||
editor.$contentElt.focus(function() {
|
||||
@ -541,38 +550,15 @@ define([
|
||||
get: function () {
|
||||
return this.textContent;
|
||||
},
|
||||
set: function (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 = inputElt.createRange(startOffset, previousTextContent.length - endOffset);
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(replacement));
|
||||
}
|
||||
set: setValue
|
||||
});
|
||||
|
||||
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', {
|
||||
get: function () {
|
||||
return selectionStart;
|
||||
},
|
||||
set: function (value) {
|
||||
inputElt.setSelectionStartEnd(value, selectionEnd);
|
||||
setSelectionStartEnd(value, selectionEnd);
|
||||
},
|
||||
|
||||
enumerable: true,
|
||||
@ -584,37 +570,15 @@ define([
|
||||
return selectionEnd;
|
||||
},
|
||||
set: function (value) {
|
||||
inputElt.setSelectionStartEnd(selectionStart, value);
|
||||
setSelectionStartEnd(selectionStart, value);
|
||||
},
|
||||
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
inputElt.setSelectionStartEnd = function(start, end) {
|
||||
selectionStart = start;
|
||||
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.setSelectionStartEnd = setSelectionStartEnd;
|
||||
inputElt.createRange = createRange;
|
||||
inputElt.getOffsetCoordinates = function(ss) {
|
||||
var offset = findOffset(ss);
|
||||
return getCoordinates(ss, offset.element, offset.offset);
|
||||
@ -631,9 +595,11 @@ define([
|
||||
return;
|
||||
}
|
||||
saveSelectionState();
|
||||
adjustCursorPosition();
|
||||
|
||||
var cmdOrCtrl = evt.metaKey || evt.ctrlKey;
|
||||
if(!cmdOrCtrl) {
|
||||
adjustCursorPosition();
|
||||
}
|
||||
|
||||
switch (evt.which) {
|
||||
case 9: // Tab
|
||||
@ -659,11 +625,11 @@ define([
|
||||
}, 0);
|
||||
})
|
||||
.on('paste', function () {
|
||||
currentMode = 'paste';
|
||||
undoManager.currentMode = 'paste';
|
||||
adjustCursorPosition();
|
||||
})
|
||||
.on('cut', function () {
|
||||
currentMode = 'cut';
|
||||
undoManager.currentMode = 'cut';
|
||||
adjustCursorPosition();
|
||||
});
|
||||
|
||||
@ -683,7 +649,7 @@ define([
|
||||
|
||||
actions[action](state, options);
|
||||
inputElt.value = state.before + state.selection + state.after;
|
||||
inputElt.setSelectionStartEnd(state.ss, state.se);
|
||||
setSelectionStartEnd(state.ss, state.se);
|
||||
$inputElt.trigger('input');
|
||||
};
|
||||
|
||||
@ -740,7 +706,7 @@ define([
|
||||
clearNewline = true;
|
||||
}
|
||||
|
||||
currentMode = 'newlines';
|
||||
undoManager.currentMode = 'newlines';
|
||||
|
||||
state.before += '\n' + indent;
|
||||
state.selection = '';
|
||||
@ -750,7 +716,6 @@ define([
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
var sectionList = [];
|
||||
var sectionsToRemove = [];
|
||||
var modifiedSections = [];
|
||||
@ -822,11 +787,11 @@ define([
|
||||
highlight(section);
|
||||
newSectionEltList.appendChild(section.elt);
|
||||
});
|
||||
noWatch(function() {
|
||||
watcher.noWatch(function() {
|
||||
if(fileChanged === true) {
|
||||
editor.contentElt.innerHTML = '';
|
||||
editor.contentElt.appendChild(newSectionEltList);
|
||||
inputElt.setSelectionStartEnd(selectionStart, selectionEnd);
|
||||
setSelectionStartEnd(selectionStart, selectionEnd);
|
||||
}
|
||||
else {
|
||||
// Remove outdated sections
|
||||
@ -852,7 +817,7 @@ define([
|
||||
childNode = nextNode;
|
||||
}
|
||||
|
||||
inputElt.setSelectionStartEnd(selectionStart, selectionEnd);
|
||||
setSelectionStartEnd(selectionStart, selectionEnd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -225,10 +225,8 @@ define([
|
||||
var $previewContentsElt;
|
||||
eventMgr.onAsyncPreview = function() {
|
||||
logger.log("onAsyncPreview");
|
||||
logger.log("Conversion time: " + (new Date() - eventMgr.previewStartTime));
|
||||
function recursiveCall(callbackList) {
|
||||
var callback = callbackList.length ? callbackList.shift() : function() {
|
||||
logger.log("Preview time: " + (new Date() - eventMgr.previewStartTime));
|
||||
_.defer(function() {
|
||||
var html = "";
|
||||
_.each(previewContentsElt.children, function(elt) {
|
||||
|
@ -12,7 +12,7 @@ define([
|
||||
buttonHtmlCode.defaultConfig = {
|
||||
template: "<%= documentHTML %>",
|
||||
};
|
||||
|
||||
|
||||
buttonHtmlCode.onLoadSettings = function() {
|
||||
utils.setInputValue("#textarea-html-code-template", buttonHtmlCode.config.template);
|
||||
};
|
||||
@ -35,28 +35,14 @@ define([
|
||||
selectedFileDesc = fileDesc;
|
||||
};
|
||||
|
||||
var textareaElt;
|
||||
buttonHtmlCode.onPreviewFinished = function(htmlWithComments, htmlWithoutComments) {
|
||||
try {
|
||||
var htmlCode = _.template(buttonHtmlCode.config.template, {
|
||||
documentTitle: selectedFileDesc.title,
|
||||
documentMarkdown: selectedFileDesc.content,
|
||||
strippedDocumentMarkdown: selectedFileDesc.content.substring(selectedFileDesc.frontMatter ? selectedFileDesc.frontMatter._frontMatter.length : 0),
|
||||
documentHTML: htmlWithoutComments,
|
||||
documentHTMLWithComments: htmlWithComments,
|
||||
frontMatter: selectedFileDesc.frontMatter,
|
||||
publishAttributes: undefined,
|
||||
});
|
||||
textareaElt.value = htmlCode;
|
||||
}
|
||||
catch(e) {
|
||||
eventMgr.onError(e);
|
||||
return e.message;
|
||||
}
|
||||
var htmlWithComments, htmlWithoutComments;
|
||||
buttonHtmlCode.onPreviewFinished = function(htmlWithCommentsParam, htmlWithoutCommentsParam) {
|
||||
htmlWithComments = htmlWithCommentsParam;
|
||||
htmlWithoutComments = htmlWithoutCommentsParam;
|
||||
};
|
||||
|
||||
buttonHtmlCode.onReady = function() {
|
||||
textareaElt = document.getElementById('input-html-code');
|
||||
var textareaElt = document.getElementById('input-html-code');
|
||||
$(".action-html-code").click(function() {
|
||||
_.defer(function() {
|
||||
$("#input-html-code").each(function() {
|
||||
@ -66,9 +52,25 @@ define([
|
||||
this.select();
|
||||
});
|
||||
});
|
||||
}).parent().on('show.bs.dropdown', function() {
|
||||
try {
|
||||
var htmlCode = _.template(buttonHtmlCode.config.template, {
|
||||
documentTitle: selectedFileDesc.title,
|
||||
documentMarkdown: selectedFileDesc.content,
|
||||
strippedDocumentMarkdown: selectedFileDesc.content.substring(selectedFileDesc.frontMatter ? selectedFileDesc.frontMatter._frontMatter.length : 0),
|
||||
documentHTML: htmlWithoutComments,
|
||||
documentHTMLWithComments: htmlWithComments,
|
||||
frontMatter: selectedFileDesc.frontMatter,
|
||||
publishAttributes: undefined,
|
||||
});
|
||||
textareaElt.value = htmlCode;
|
||||
}
|
||||
catch(e) {
|
||||
eventMgr.onError(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return buttonHtmlCode;
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -20,7 +20,7 @@ define([
|
||||
].join('');
|
||||
var popoverTitleTmpl = [
|
||||
'<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>',
|
||||
' </a>',
|
||||
' <%- title %>',
|
||||
@ -35,7 +35,11 @@ define([
|
||||
var offsetMap = {};
|
||||
function setCommentEltCoordinates(commentElt, y) {
|
||||
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';
|
||||
commentElt.style.top = top;
|
||||
commentElt.style.right = right;
|
||||
@ -46,7 +50,7 @@ define([
|
||||
var marginElt;
|
||||
var commentEltList = [];
|
||||
var newCommentElt = crel('a', {
|
||||
class: 'icon-comment new'
|
||||
class: 'discussion icon-comment new'
|
||||
});
|
||||
var cursorY;
|
||||
comments.onCursorCoordinates = function(x, y) {
|
||||
@ -69,6 +73,7 @@ define([
|
||||
|
||||
var cssApplier;
|
||||
var currentFileDesc;
|
||||
var refreshTimeoutId;
|
||||
var refreshDiscussions = _.debounce(function() {
|
||||
if(currentFileDesc === undefined) {
|
||||
return;
|
||||
@ -80,11 +85,28 @@ define([
|
||||
});
|
||||
commentEltList = [];
|
||||
offsetMap = {};
|
||||
_.each(currentFileDesc.discussionList, function(discussion) {
|
||||
var isReplied = _.last(discussion.commentList).author != author;
|
||||
var discussionList = _.values(currentFileDesc.discussionList);
|
||||
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', {
|
||||
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;
|
||||
var coordinates = inputElt.getOffsetCoordinates(discussion.selectionEnd);
|
||||
var lineIndex = setCommentEltCoordinates(commentElt, coordinates.y);
|
||||
@ -96,14 +118,10 @@ define([
|
||||
inputElt.scrollTop += parseInt(commentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
|
||||
movePopover(commentElt);
|
||||
}
|
||||
});
|
||||
|
||||
// Move newCommentElt
|
||||
setCommentEltCoordinates(newCommentElt, cursorY);
|
||||
if(currentContext && !currentContext.discussion.discussionIndex) {
|
||||
inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
|
||||
movePopover(newCommentElt);
|
||||
refreshTimeoutId = setTimeout(refreshOne, 5);
|
||||
}
|
||||
clearTimeout(refreshTimeoutId);
|
||||
refreshTimeoutId = setTimeout(refreshOne, 5);
|
||||
}, 50);
|
||||
|
||||
comments.onFileOpen = function(fileDesc) {
|
||||
@ -155,7 +173,7 @@ define([
|
||||
|
||||
comments.onDiscussionRemoved = function(fileDesc, discussion) {
|
||||
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) {
|
||||
closeCurrentPopover();
|
||||
}
|
||||
@ -168,6 +186,9 @@ define([
|
||||
};
|
||||
|
||||
function getDiscussionComments() {
|
||||
if(currentContext.discussion.type == 'conflict') {
|
||||
return '';
|
||||
}
|
||||
return currentContext.discussion.commentList.map(function(comment) {
|
||||
return _.template(commentTmpl, {
|
||||
author: comment.author || 'Anonymous',
|
||||
@ -206,16 +227,18 @@ define([
|
||||
title += '...';
|
||||
}
|
||||
return _.template(popoverTitleTmpl, {
|
||||
title: title
|
||||
title: title,
|
||||
type: currentContext.discussion.type
|
||||
});
|
||||
},
|
||||
content: function() {
|
||||
var content = _.template(commentsPopoverContentHTML, {
|
||||
commentList: getDiscussionComments()
|
||||
commentList: getDiscussionComments(),
|
||||
type: currentContext.discussion.type
|
||||
});
|
||||
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) {
|
||||
closeCurrentPopover();
|
||||
var context = {
|
||||
@ -299,7 +322,7 @@ define([
|
||||
// Create discussion index
|
||||
var discussionIndex;
|
||||
do {
|
||||
discussionIndex = utils.randomString();
|
||||
discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision
|
||||
} while(_.has(discussionList, discussionIndex));
|
||||
context.discussion.discussionIndex = discussionIndex;
|
||||
discussionList[discussionIndex] = context.discussion;
|
||||
@ -316,8 +339,14 @@ define([
|
||||
var $removeButton = $(context.popoverElt.querySelector('.action-remove-discussion'));
|
||||
if(evt.target.discussionIndex) {
|
||||
// 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() {
|
||||
$(context.popoverElt.querySelector('.new-comment-block')).addClass('hide');
|
||||
$(context.popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide');
|
||||
|
@ -2,8 +2,10 @@ define([
|
||||
"underscore",
|
||||
"extensions/markdownExtra",
|
||||
"extensions/mathJax",
|
||||
"extensions/partialRendering",
|
||||
"classes/Extension",
|
||||
], function(_, markdownExtra, mathJax, Extension) {
|
||||
"crel",
|
||||
], function(_, markdownExtra, mathJax, partialRendering, Extension, crel) {
|
||||
|
||||
var markdownSectionParser = new Extension("markdownSectionParser", "Markdown section parser");
|
||||
|
||||
@ -13,6 +15,7 @@ define([
|
||||
};
|
||||
|
||||
var sectionList = [];
|
||||
var previewContentsElt;
|
||||
|
||||
// Regexp to look for section 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');
|
||||
|
||||
var converter = editor.getConverter();
|
||||
converter.hooks.chain("preConversion", function() {
|
||||
return _.reduce(sectionList, function(result, section) {
|
||||
return result + section.previewText;
|
||||
}, '');
|
||||
});
|
||||
if(!partialRendering.enabled) {
|
||||
converter.hooks.chain("preConversion", function() {
|
||||
return _.reduce(sectionList, function(result, section) {
|
||||
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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="discussion-comment-list"><%= commentList %></div>
|
||||
<div class="new-comment-block">
|
||||
<div class="new-comment-block<%= !type ? '': ' hide' %>">
|
||||
<div class="form-group">
|
||||
<input class="form-control input-comment-author" placeholder="Your name"></input>
|
||||
<textarea class="form-control input-comment-content"></textarea>
|
||||
@ -8,6 +8,12 @@
|
||||
<button class="btn btn-primary action-add-comment">Add</button>
|
||||
</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">
|
||||
<br/>
|
||||
<blockquote>Remove this discussion, really?</blockquote>
|
||||
|
@ -56,8 +56,8 @@ define([
|
||||
var syncAttributes = createSyncAttributes(file.path, file.versionTag, file.content);
|
||||
var syncLocations = {};
|
||||
syncLocations[syncAttributes.syncIndex] = syncAttributes;
|
||||
var parsingResult = dropboxProvider.parseSerializedContent(file.content);
|
||||
var fileDesc = fileMgr.createFile(file.name, parsingResult.content, parsingResult.discussionList, syncLocations);
|
||||
var parsedContent = dropboxProvider.parseSerializedContent(file.content);
|
||||
var fileDesc = fileMgr.createFile(file.name, parsedContent.content, parsedContent.discussionList, syncLocations);
|
||||
fileMgr.selectFile(fileDesc);
|
||||
fileDescList.push(fileDesc);
|
||||
});
|
||||
@ -165,7 +165,12 @@ define([
|
||||
callback(error);
|
||||
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 syncIndex = syncAttributes.syncIndex;
|
||||
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
|
||||
@ -173,48 +178,30 @@ define([
|
||||
if(fileDesc === undefined) {
|
||||
return;
|
||||
}
|
||||
var localTitle = fileDesc.title;
|
||||
// File deleted
|
||||
if(change.wasRemoved === true) {
|
||||
eventMgr.onError('"' + localTitle + '" has been removed from Dropbox.');
|
||||
eventMgr.onError('"' + fileDesc.title + '" has been removed from Dropbox.');
|
||||
fileDesc.removeSyncLocation(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 parsingResult = dropboxProvider.parseSerializedContent(file.content);
|
||||
var remoteContent = parsingResult.content;
|
||||
var remoteDiscussionList = parsingResult.discussionList;
|
||||
var remoteContentCRC = utils.crc32(remoteContent);
|
||||
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
|
||||
}
|
||||
}
|
||||
var parsedContent = dropboxProvider.parseSerializedContent(file.content);
|
||||
var remoteContent = parsedContent.content;
|
||||
var remoteDiscussionList = parsedContent.discussionList;
|
||||
var remoteCRC = dropboxProvider.syncMerge(fileDesc, syncAttributes, remoteContent, fileDesc.title, remoteDiscussionList);
|
||||
// Update syncAttributes
|
||||
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);
|
||||
});
|
||||
storage[PROVIDER_DROPBOX + ".lastChangeId"] = newChangeId;
|
||||
callback();
|
||||
setTimeout(merge, 5);
|
||||
}
|
||||
setTimeout(merge, 5);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -56,8 +56,8 @@ define([
|
||||
syncAttributes.isRealtime = file.isRealtime;
|
||||
var syncLocations = {};
|
||||
syncLocations[syncAttributes.syncIndex] = syncAttributes;
|
||||
var parsingResult = gdriveProvider.parseSerializedContent(file.content);
|
||||
fileDesc = fileMgr.createFile(file.title, parsingResult.content, parsingResult.discussionList, syncLocations);
|
||||
var parsedContent = gdriveProvider.parseSerializedContent(file.content);
|
||||
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionList, syncLocations);
|
||||
fileDescList.push(fileDesc);
|
||||
});
|
||||
if(fileDesc !== undefined) {
|
||||
@ -200,19 +200,22 @@ define([
|
||||
callback(error);
|
||||
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 syncIndex = syncAttributes.syncIndex;
|
||||
var fileDesc = fileMgr.getFileFromSyncIndex(syncIndex);
|
||||
// No file corresponding (file may have been deleted
|
||||
// locally)
|
||||
// No file corresponding (file may have been deleted locally)
|
||||
if(fileDesc === undefined) {
|
||||
return;
|
||||
}
|
||||
var localTitle = fileDesc.title;
|
||||
// File deleted
|
||||
if(change.deleted === true) {
|
||||
eventMgr.onError('"' + localTitle + '" has been removed from ' + providerName + '.');
|
||||
eventMgr.onError('"' + fileDesc.title + '" has been removed from ' + providerName + '.');
|
||||
fileDesc.removeSyncLocation(syncAttributes);
|
||||
eventMgr.onSyncRemoved(fileDesc, syncAttributes);
|
||||
if(syncAttributes.isRealtime === true && fileMgr.currentFile === fileDesc) {
|
||||
@ -220,46 +223,28 @@ define([
|
||||
}
|
||||
return;
|
||||
}
|
||||
var localTitleChanged = syncAttributes.titleCRC != utils.crc32(localTitle);
|
||||
var localContent = fileDesc.content;
|
||||
var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent);
|
||||
var file = change.file;
|
||||
var remoteTitleCRC = utils.crc32(file.title);
|
||||
var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC;
|
||||
var fileTitleChanged = localTitle != file.title;
|
||||
var remoteContentCRC = utils.crc32(file.content);
|
||||
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
|
||||
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
|
||||
}
|
||||
}
|
||||
var parsedContent = gdriveProvider.parseSerializedContent(file.content);
|
||||
var remoteContent = parsedContent.content;
|
||||
var remoteTitle = file.title;
|
||||
var remoteDiscussionList = parsedContent.discussionList;
|
||||
var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList);
|
||||
|
||||
// Update syncAttributes
|
||||
syncAttributes.etag = file.etag;
|
||||
if(!syncAttributes.isRealtime) {
|
||||
syncAttributes.contentCRC = remoteContentCRC;
|
||||
if(merge === true) {
|
||||
// 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);
|
||||
});
|
||||
storage[accountId + ".gdrive.lastChangeId"] = newChangeId;
|
||||
callback();
|
||||
setTimeout(merge, 5);
|
||||
}
|
||||
setTimeout(merge, 5);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -151,7 +151,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
}
|
||||
|
||||
pre {
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
p,
|
||||
@ -234,7 +234,7 @@ blockquote {
|
||||
font-size: 1em;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
|
||||
ul:last-child,
|
||||
ol:last-child {
|
||||
margin-bottom: 0;
|
||||
|
@ -22,7 +22,7 @@
|
||||
@secondary-bg: lighten(@secondary-desaturated, 45%);
|
||||
@secondary-bg-light: lighten(@secondary-desaturated, 47%);
|
||||
@secondary-bg-lighter: #fff;
|
||||
@secondary-color: @primary-desaturated;
|
||||
@secondary-color: lighten(@primary-desaturated, 10%);
|
||||
@secondary-color-dark: darken(@secondary-color, 12.5%);
|
||||
@secondary-color-darker: darken(@secondary-color, 25%);
|
||||
@secondary-color-darkest: darken(@secondary-color, 37.5%);
|
||||
@ -34,7 +34,7 @@
|
||||
@tertiary-bg: #fff;
|
||||
@tertiary-color-lighter: fade(@tertiary-color, 40%);
|
||||
@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-darker: darken(@tertiary-color, 30%);
|
||||
|
||||
@ -1050,30 +1050,41 @@ a {
|
||||
background-color: @tertiary-bg;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
> .editor-content {
|
||||
padding-bottom: 230px;
|
||||
}
|
||||
> .editor-margin {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
.icon-comment {
|
||||
.discussion {
|
||||
font-size: 18px;
|
||||
&.new {
|
||||
color: fade(@tertiary-color, 10%);
|
||||
&:hover, &.active, &.active:hover {
|
||||
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 {
|
||||
color: fade(@label-danger-bg, 45%);
|
||||
&:hover, &.active, &.active:hover {
|
||||
color: fade(@label-danger-bg, 80%) !important;
|
||||
}
|
||||
}
|
||||
&.added {
|
||||
color: fade(@label-warning-bg, 45%);
|
||||
&.icon-fork {
|
||||
font-size: 22px;
|
||||
color: fade(@label-danger-bg, 70%);
|
||||
&:hover, &.active, &.active:hover {
|
||||
color: fade(@label-warning-bg, 80%) !important;
|
||||
color: @label-danger-bg !important;
|
||||
}
|
||||
&:before {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
position: absolute;
|
||||
|
@ -78,8 +78,7 @@ define([
|
||||
|
||||
// No more synchronized location for this document
|
||||
if(uploadSyncAttributesList.length === 0) {
|
||||
fileUp(callback);
|
||||
return;
|
||||
return fileUp(callback);
|
||||
}
|
||||
|
||||
// Dequeue a synchronized location
|
||||
@ -98,16 +97,15 @@ define([
|
||||
uploadTitle,
|
||||
uploadTitleCRC,
|
||||
uploadDiscussionList,
|
||||
uploadTitleCRC,
|
||||
uploadDiscussionListCRC,
|
||||
syncAttributes,
|
||||
function(error, uploadFlag) {
|
||||
if(uploadFlag === true) {
|
||||
// If uploadFlag is true, request another upload cycle
|
||||
uploadCycle = true;
|
||||
}
|
||||
if(error) {
|
||||
callback(error);
|
||||
return;
|
||||
return callback(error);
|
||||
}
|
||||
if(uploadFlag) {
|
||||
// Update syncAttributes in storage
|
||||
@ -124,16 +122,14 @@ define([
|
||||
|
||||
// No more fileDesc to synchronize
|
||||
if(uploadFileList.length === 0) {
|
||||
syncUp(callback);
|
||||
return;
|
||||
return syncUp(callback);
|
||||
}
|
||||
|
||||
// Dequeue a fileDesc to synchronize
|
||||
var fileDesc = uploadFileList.pop();
|
||||
uploadSyncAttributesList = _.values(fileDesc.syncLocations);
|
||||
if(uploadSyncAttributesList.length === 0) {
|
||||
fileUp(callback);
|
||||
return;
|
||||
return fileUp(callback);
|
||||
}
|
||||
|
||||
// Get document title/content
|
||||
@ -164,22 +160,19 @@ define([
|
||||
var providerList = [];
|
||||
function providerDown(callback) {
|
||||
if(providerList.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
return callback();
|
||||
}
|
||||
var provider = providerList.pop();
|
||||
|
||||
// Check that provider has files to sync
|
||||
if(!synchronizer.hasSync(provider)) {
|
||||
providerDown(callback);
|
||||
return;
|
||||
return providerDown(callback);
|
||||
}
|
||||
|
||||
// Perform provider's syncDown
|
||||
provider.syncDown(function(error) {
|
||||
if(error) {
|
||||
callback(error);
|
||||
return;
|
||||
return callback(error);
|
||||
}
|
||||
providerDown(callback);
|
||||
});
|
||||
@ -354,8 +347,7 @@ define([
|
||||
|
||||
if(isRealtime) {
|
||||
if(_.size(fileDesc.syncLocations) > 0) {
|
||||
eventMgr.onError("Real time collaborative document can't be synchronized with multiple locations");
|
||||
return;
|
||||
return eventMgr.onError("Real time collaborative document can't be synchronized with multiple locations");
|
||||
}
|
||||
// Perform the provider's real time export
|
||||
provider.exportRealtimeFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) {
|
||||
|
Loading…
Reference in New Issue
Block a user