From 4ae6e540d4e2ff392b16172e0d14c04a6c3b7d53 Mon Sep 17 00:00:00 2001 From: benweet Date: Sun, 30 Mar 2014 02:44:51 +0100 Subject: [PATCH] Fixed sync merge --- public/res/classes/Provider.js | 143 +++++++-- public/res/editor.js | 287 ++++++++---------- public/res/eventMgr.js | 2 - public/res/extensions/buttonHtmlCode.js | 44 +-- public/res/extensions/comments.js | 69 +++-- .../res/extensions/markdownSectionParser.js | 52 +++- public/res/html/commentsPopoverContent.html | 8 +- public/res/providers/dropboxProvider.js | 59 ++-- public/res/providers/gdriveProviderBuilder.js | 69 ++--- public/res/styles/base.less | 4 +- public/res/styles/main.less | 25 +- public/res/synchronizer.js | 26 +- 12 files changed, 449 insertions(+), 339 deletions(-) diff --git a/public/res/classes/Provider.js b/public/res/classes/Provider.js index 04009895..31931f1f 100644 --- a/public/res/classes/Provider.js +++ b/public/res/classes/Provider.js @@ -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 + ''; } 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; diff --git a/public/res/editor.js b/public/res/editor.js index 162e70cc..da50ddfc 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -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); } }); } diff --git a/public/res/eventMgr.js b/public/res/eventMgr.js index a6a82850..e0e78827 100644 --- a/public/res/eventMgr.js +++ b/public/res/eventMgr.js @@ -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) { diff --git a/public/res/extensions/buttonHtmlCode.js b/public/res/extensions/buttonHtmlCode.js index 9ec660cc..7b22656b 100644 --- a/public/res/extensions/buttonHtmlCode.js +++ b/public/res/extensions/buttonHtmlCode.js @@ -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; -}); \ No newline at end of file +}); diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index e6024d27..b366cd91 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -20,7 +20,7 @@ define([ ].join(''); var popoverTitleTmpl = [ '', - ' ', + ' ', ' ', ' ', ' <%- 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'); diff --git a/public/res/extensions/markdownSectionParser.js b/public/res/extensions/markdownSectionParser.js index 8ad922ae..6f6f781e 100644 --- a/public/res/extensions/markdownSectionParser.js +++ b/public/res/extensions/markdownSectionParser.js @@ -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
\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; diff --git a/public/res/html/commentsPopoverContent.html b/public/res/html/commentsPopoverContent.html index bec86cd9..0e8fc5b0 100644 --- a/public/res/html/commentsPopoverContent.html +++ b/public/res/html/commentsPopoverContent.html @@ -1,5 +1,5 @@
<%= commentList %>
-
+
@@ -8,6 +8,12 @@
+
+

Multiple users have made conflicting modifications that you have to review.

+
+ +
+

Remove this discussion, really?
diff --git a/public/res/providers/dropboxProvider.js b/public/res/providers/dropboxProvider.js index a7db4da2..1e704bb6 100644 --- a/public/res/providers/dropboxProvider.js +++ b/public/res/providers/dropboxProvider.js @@ -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); }); }); }; diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js index b4ec687a..d6149030 100644 --- a/public/res/providers/gdriveProviderBuilder.js +++ b/public/res/providers/gdriveProviderBuilder.js @@ -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); }); }); }; diff --git a/public/res/styles/base.less b/public/res/styles/base.less index cabfe718..e24c07fd 100644 --- a/public/res/styles/base.less +++ b/public/res/styles/base.less @@ -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; diff --git a/public/res/styles/main.less b/public/res/styles/main.less index eb9a5896..6f100a38 100644 --- a/public/res/styles/main.less +++ b/public/res/styles/main.less @@ -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; diff --git a/public/res/synchronizer.js b/public/res/synchronizer.js index ff824ba7..b5aed298 100644 --- a/public/res/synchronizer.js +++ b/public/res/synchronizer.js @@ -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) {