Fixed sync merge
This commit is contained in:
parent
acebad8a65
commit
4ae6e540d4
@ -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() {
|
||||||
else {
|
if(contentChanged) {
|
||||||
fileDesc.content = remoteContent;
|
if(!/\n$/.test(remoteContent)) {
|
||||||
eventMgr.onContentChanged(fileDesc, remoteContent);
|
remoteContent += '\n';
|
||||||
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + this.providerName + '.');
|
}
|
||||||
}
|
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;
|
return Provider;
|
||||||
|
@ -63,23 +63,76 @@ define([
|
|||||||
fileDesc = selectedFileDesc;
|
fileDesc = selectedFileDesc;
|
||||||
});
|
});
|
||||||
|
|
||||||
var contentObserver;
|
// Watcher used to detect editor changes
|
||||||
var isWatching = false;
|
function Watcher() {
|
||||||
function noWatch(cb) {
|
this.isWatching = false;
|
||||||
if(isWatching === true) {
|
var contentObserver;
|
||||||
contentObserver.disconnect();
|
this.startWatching = function() {
|
||||||
isWatching = false;
|
this.isWatching = true;
|
||||||
cb();
|
contentObserver = contentObserver || new MutationObserver(checkContentChange);
|
||||||
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 {
|
||||||
|
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 {
|
range.setEnd(offset.element, offset.offset);
|
||||||
cb();
|
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 {
|
|
||||||
element: element,
|
|
||||||
offset: ss - offset
|
|
||||||
};
|
|
||||||
} else if (container) {
|
|
||||||
element = container;
|
|
||||||
|
|
||||||
while (element && element.lastChild) {
|
|
||||||
element = element.lastChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.nodeType === 3) {
|
|
||||||
return {
|
return {
|
||||||
element: element,
|
element: walker.currentNode,
|
||||||
offset: element.textContent.length
|
offset: offset
|
||||||
};
|
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -12,7 +12,7 @@ define([
|
|||||||
buttonHtmlCode.defaultConfig = {
|
buttonHtmlCode.defaultConfig = {
|
||||||
template: "<%= documentHTML %>",
|
template: "<%= documentHTML %>",
|
||||||
};
|
};
|
||||||
|
|
||||||
buttonHtmlCode.onLoadSettings = function() {
|
buttonHtmlCode.onLoadSettings = function() {
|
||||||
utils.setInputValue("#textarea-html-code-template", buttonHtmlCode.config.template);
|
utils.setInputValue("#textarea-html-code-template", buttonHtmlCode.config.template);
|
||||||
};
|
};
|
||||||
@ -35,28 +35,14 @@ define([
|
|||||||
selectedFileDesc = fileDesc;
|
selectedFileDesc = fileDesc;
|
||||||
};
|
};
|
||||||
|
|
||||||
var textareaElt;
|
var htmlWithComments, htmlWithoutComments;
|
||||||
buttonHtmlCode.onPreviewFinished = function(htmlWithComments, htmlWithoutComments) {
|
buttonHtmlCode.onPreviewFinished = function(htmlWithCommentsParam, htmlWithoutCommentsParam) {
|
||||||
try {
|
htmlWithComments = htmlWithCommentsParam;
|
||||||
var htmlCode = _.template(buttonHtmlCode.config.template, {
|
htmlWithoutComments = htmlWithoutCommentsParam;
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
buttonHtmlCode.onReady = function() {
|
buttonHtmlCode.onReady = function() {
|
||||||
textareaElt = document.getElementById('input-html-code');
|
var textareaElt = document.getElementById('input-html-code');
|
||||||
$(".action-html-code").click(function() {
|
$(".action-html-code").click(function() {
|
||||||
_.defer(function() {
|
_.defer(function() {
|
||||||
$("#input-html-code").each(function() {
|
$("#input-html-code").each(function() {
|
||||||
@ -66,9 +52,25 @@ define([
|
|||||||
this.select();
|
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;
|
return buttonHtmlCode;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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');
|
||||||
|
@ -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();
|
||||||
converter.hooks.chain("preConversion", function() {
|
if(!partialRendering.enabled) {
|
||||||
return _.reduce(sectionList, function(result, section) {
|
converter.hooks.chain("preConversion", function() {
|
||||||
return result + section.previewText;
|
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;
|
var fileDesc;
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -151,7 +151,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
word-break: break-all;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
p,
|
||||||
@ -234,7 +234,7 @@ blockquote {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
line-height: @line-height-base;
|
line-height: @line-height-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul:last-child,
|
ul:last-child,
|
||||||
ol:last-child {
|
ol:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user