Fixed sync merge

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

View File

@ -4,9 +4,10 @@ define([
'settings',
'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;

View File

@ -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);
}
});
}

View File

@ -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) {

View File

@ -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;
});
});

View File

@ -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');

View File

@ -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;

View File

@ -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>

View File

@ -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);
});
});
};

View File

@ -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);
});
});
};

View File

@ -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;

View File

@ -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;

View File

@ -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) {