diff --git a/public/res/classes/Provider.js b/public/res/classes/Provider.js index 31931f1f..a0c437bc 100644 --- a/public/res/classes/Provider.js +++ b/public/res/classes/Provider.js @@ -72,9 +72,6 @@ define([ objectHash: function(obj) { return JSON.stringify(obj); }, - arrays: { - detectMove: false, - }, textDiff: { minLength: 9999999 } @@ -82,25 +79,75 @@ define([ var merge = settings.conflictMode == 'merge'; Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionListJSON) { - var lineArray = []; - var lineHash = {}; - function linesToChars(text) { - var chars = ''; - var lineArrayLength = lineArray.length; - text.split('\n').forEach(function(line) { - if(lineHash.hasOwnProperty(line)) { - chars += String.fromCharCode(lineHash[line]); - } else { - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; + function cleanupDiffs(diffs) { + var result = []; + var removeDiff = [-1, '']; + var addDiff = [1, '']; + var distance = 20; + diffs.forEach(function(diff) { + var diffType = diff[0]; + var diffText = diff[1]; + if(diffType === 0) { + if(diffText.length > distance) { + if(removeDiff[1] || addDiff[1]) { + var match = /\S+/.exec(diffText); + if(match) { + var prefixOffset = match.index + match[0].length; + var prefix = diffText.substring(0, prefixOffset); + diffText = diffText.substring(prefixOffset); + removeDiff[1] += prefix; + addDiff[1] += prefix; + } + } + if(diffText) { + var suffixOffset = diffText.length; + while(suffixOffset && /\S/.test(diffText[suffixOffset - 1])) { + suffixOffset--; + } + var suffix = diffText.substring(suffixOffset); + diffText = diffText.substring(0, suffixOffset); + if(diffText.length > distance) { + removeDiff[1] && result.push(removeDiff); + removeDiff = [-1, '']; + addDiff[1] && result.push(addDiff); + addDiff = [1, '']; + result.push([0, diffText]); + } + else { + removeDiff[1] += diffText; + addDiff[1] += diffText; + } + removeDiff[1] += suffix; + addDiff[1] += suffix; + } + } + else { + removeDiff[1] += diffText; + addDiff[1] += diffText; + } + } + else if(diffType === -1) { + removeDiff[1] += diffText; + } + else if(diffType === 1) { + addDiff[1] += diffText; } }); - return chars; + if(removeDiff[1] == addDiff[1]) { + result.push([0, addDiff[1]]); + } + else { + removeDiff[1] && result.push(removeDiff); + addDiff[1] && result.push(addDiff); + } + return result; } function moveComments(oldTextContent, newTextContent, discussionList) { + if(!discussionList.length) { + return; + } var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent); var changed = false; var startOffset = 0; @@ -121,20 +168,20 @@ define([ // selectionEnd if(discussion.selectionEnd >= endOffset) { discussion.selectionEnd += diffOffset; - changed = true; + discussion.discussionIndex && (changed = true); } else if(discussion.selectionEnd > startOffset) { discussion.selectionEnd = startOffset; - changed = true; + discussion.discussionIndex && (changed = true); } // selectionStart if(discussion.selectionStart >= endOffset) { discussion.selectionStart += diffOffset; - changed = true; + discussion.discussionIndex && (changed = true); } else if(discussion.selectionStart > startOffset) { discussion.selectionStart = startOffset; - changed = true; + discussion.discussionIndex && (changed = true); } }); startOffset = endOffset; @@ -157,28 +204,32 @@ define([ var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON); // Check content - var contentChanged = localContent != remoteContent; var localContentChanged = syncAttributes.contentCRC != localContentCRC; var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; - var contentConflict = contentChanged && localContentChanged && remoteContentChanged; - contentChanged = contentChanged && remoteContentChanged; + var contentChanged = localContent != remoteContent && remoteContentChanged; + var contentConflict = contentChanged && localContentChanged; // Check title syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox - var titleChanged = localTitle != remoteTitle; var localTitleChanged = syncAttributes.titleCRC != localTitleCRC; var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC; - var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged; - titleChanged = titleChanged && remoteTitleChanged; + var titleChanged = localTitle != remoteTitle && remoteTitleChanged; + var titleConflict = titleChanged && localTitleChanged; // Check discussionList - var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON; var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC; var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC; - var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged; - discussionListChanged = discussionListChanged && remoteDiscussionListChanged; + var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON && remoteDiscussionListChanged; + var discussionListConflict = discussionListChanged && localDiscussionListChanged; var conflictList = []; + var newContent = remoteContent; + var newTitle = remoteTitle; + var newDiscussionList = remoteDiscussionList; + var adjustLocalDiscussionList = false; + var adjustRemoteDiscussionList = false; + var mergeDiscussionList = false; + var diffs, patch; if( (!merge && (contentConflict || titleConflict || discussionListConflict)) || (contentConflict && syncAttributes.content === undefined) || @@ -189,25 +240,21 @@ define([ eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); } else { - var oldDiscussionList; - var patch, delta; if(contentConflict) { - // Patch content (line mode) + // Patch content var oldContent = syncAttributes.content; - /* - var oldContentLines = linesToChars(syncAttributes.content); - var localContentLines = linesToChars(localContent); - var remoteContentLines = linesToChars(remoteContent); - */ - patch = diffMatchPatch.patch_make(oldContent, localContent); + diffs = diffMatchPatch.diff_main(oldContent, localContent); + diffMatchPatch.diff_cleanupSemantic(diffs); + patch = diffMatchPatch.patch_make(oldContent, diffs); var patchResult = diffMatchPatch.patch_apply(patch, remoteContent); - var newContent = patchResult[0]; + 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); + // Remaining conflicts + diffs = diffMatchPatch.diff_main(localContent, newContent); + diffs = cleanupDiffs(diffs); + newContent = ''; var conflict; diffs.forEach(function(diff) { @@ -231,84 +278,105 @@ define([ 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); - discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray); + if(contentChanged) { + if(localDiscussionListChanged) { + adjustLocalDiscussionList = true; } - if(remoteDiscussionListChanged) { - // Move remote discussion according to content patch - var remoteDiscussionArray = _.values(remoteDiscussionList); - moveComments(remoteContent, newContent, remoteDiscussionArray); - - if(localDiscussionListChanged) { - // Patch remote discussionList with local modifications - oldDiscussionList = JSON.parse(syncAttributes.discussionList); - delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList); - jsonDiffPatch.patch(remoteDiscussionList, delta); - } + adjustRemoteDiscussionList = true; } else { - remoteDiscussionList = localDiscussionList; + adjustLocalDiscussionList = true; + newDiscussionList = 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) { - // Patch remote discussionList with local modifications - oldDiscussionList = JSON.parse(syncAttributes.discussionList); - delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList); - jsonDiffPatch.patch(remoteDiscussionList, delta); + + if(discussionListConflict) { + mergeDiscussionList = true; } + if(titleConflict) { // Patch title patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle); - remoteTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0]; + newTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0]; } } + // Adjust local discussions offset + var editorSelection; + if(contentChanged) { + var localDiscussionArray = []; + // Adjust editor's cursor position and local discussions at the same time + if(fileMgr.currentFile === fileDesc) { + editorSelection = { + selectionStart: editor.inputElt.selectionStart, + selectionEnd: editor.inputElt.selectionEnd + }; + localDiscussionArray.push(editorSelection); + fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion); + } + if(adjustLocalDiscussionList) { + localDiscussionArray = localDiscussionArray.concat(_.values(localDiscussionList)); + } + discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray); + } + + // Adjust remote discussions offset + if(adjustRemoteDiscussionList) { + var remoteDiscussionArray = _.values(remoteDiscussionList); + moveComments(remoteContent, newContent, remoteDiscussionArray); + } + + // Patch remote discussionList with local modifications + if(mergeDiscussionList) { + var oldDiscussionList = JSON.parse(syncAttributes.discussionList); + diffs = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList); + jsonDiffPatch.patch(remoteDiscussionList, diffs); + _.each(remoteDiscussionList, function(discussion, discussionIndex) { + if(!discussion) { + delete remoteDiscussionList[discussionIndex]; + } + }); + } + + 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(newDiscussionList, discussionIndex)); + conflict.discussionIndex = discussionIndex; + newDiscussionList[discussionIndex] = conflict; + }); + } + if(titleChanged) { - fileDesc.title = remoteTitle; + fileDesc.title = newTitle; eventMgr.onTitleChanged(fileDesc); - eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.'); + eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + newTitle + '" on ' + this.providerName + '.'); } if(contentChanged || discussionListChanged) { var self = this; editor.watcher.noWatch(function() { if(contentChanged) { - if(!/\n$/.test(remoteContent)) { - remoteContent += '\n'; + if(!/\n$/.test(newContent)) { + newContent += '\n'; } if(fileMgr.currentFile === fileDesc) { - editor.setValueNoWatch(remoteContent); + editor.setValueNoWatch(newContent); + editorSelection && editor.inputElt.setSelectionStartEnd( + editorSelection.selectionStart, + editorSelection.selectionEnd + ); } - fileDesc.content = remoteContent; - eventMgr.onContentChanged(fileDesc, remoteContent); + fileDesc.content = newContent; + eventMgr.onContentChanged(fileDesc, newContent); } if(discussionListChanged) { fileDesc.discussionList = remoteDiscussionList; @@ -331,7 +399,7 @@ define([ editor.undoManager.saveState(); eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + self.providerName + '.'); if(conflictList.length) { - eventMgr.onMessage('"' + remoteTitle + '" contains conflicts you need to review.'); + eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.'); } }); } diff --git a/public/res/editor.js b/public/res/editor.js index da50ddfc..082661ab 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -24,6 +24,10 @@ define([ var scrollTop = 0; var inputElt; var $inputElt; + var contentElt; + var $contentElt; + var marginElt; + var $marginElt; var previewElt; var pagedownEditor; var refreshPreviewLater = (function() { @@ -70,7 +74,7 @@ define([ this.startWatching = function() { this.isWatching = true; contentObserver = contentObserver || new MutationObserver(checkContentChange); - contentObserver.observe(editor.contentElt, { + contentObserver.observe(contentElt, { childList: true, subtree: true, characterData: true @@ -113,11 +117,14 @@ define([ } editor.setValueNoWatch = setValueNoWatch; - function setSelectionStartEnd(start, end) { + function setSelectionStartEnd(start, end, applySelection) { selectionStart = start; selectionEnd = end; fileDesc.editorStart = selectionStart; fileDesc.editorEnd = selectionEnd; + if(applySelection === false) { + return; + } var range = createRange(start, end); var selection = window.getSelection(); selection.removeAllRanges(); @@ -270,7 +277,7 @@ define([ }; this.currentMode = undefined; lastMode = undefined; - editor.contentElt.textContent = content; + contentElt.textContent = content; }; } var undoManager = new UndoManager(); @@ -393,7 +400,7 @@ define([ } function findOffset(offset) { - var walker = document.createTreeWalker(editor.contentElt, 4); + var walker = document.createTreeWalker(contentElt, 4); while(walker.nextNode()) { var text = walker.currentNode.nodeValue || ''; if (text.length > offset) { @@ -405,7 +412,7 @@ define([ offset -= text.length; } return { - element: editor.contentElt, + element: contentElt, offset: 0, error: true }; @@ -505,20 +512,26 @@ define([ editor.init = function(elt1, elt2) { inputElt = elt1; $inputElt = $(inputElt); + editor.inputElt = inputElt; + editor.$inputElt = $inputElt; + previewElt = elt2; - editor.contentElt = crel('div', { + contentElt = crel('div', { class: 'editor-content', contenteditable: true }); - inputElt.appendChild(editor.contentElt); - editor.$contentElt = $(editor.contentElt); + inputElt.appendChild(contentElt); + editor.contentElt = contentElt; + $contentElt = $(contentElt); + editor.$contentElt = $contentElt; - editor.marginElt = crel('div', { + marginElt = crel('div', { class: 'editor-margin' }); - inputElt.appendChild(editor.marginElt); - editor.$marginElt = $(editor.marginElt); + inputElt.appendChild(marginElt); + $marginElt = $(marginElt); + editor.$marginElt = $marginElt; watcher.startWatching(); @@ -535,14 +548,14 @@ define([ }); inputElt.focus = function() { - editor.$contentElt.focus(); + $contentElt.focus(); setSelectionStartEnd(selectionStart, selectionEnd); inputElt.scrollTop = scrollTop; }; - editor.$contentElt.focus(function() { + $contentElt.focus(function() { inputElt.focused = true; }); - editor.$contentElt.blur(function() { + $contentElt.blur(function() { inputElt.focused = false; }); @@ -585,7 +598,7 @@ define([ }; var clearNewline = false; - editor.$contentElt.on('keydown', function (evt) { + $contentElt.on('keydown', function (evt) { if( evt.which === 17 || // Ctrl evt.which === 91 || // Cmd @@ -742,7 +755,7 @@ define([ // Check modified section.textWithFrontMatter != newSection.textWithFrontMatter || // Check that section has not been detached or moved - section.elt.parentNode !== editor.contentElt || + section.elt.parentNode !== contentElt || // Check also the content since nodes can be injected in sections via copy/paste section.elt.textContent != newSection.textWithFrontMatter) { leftIndex = index; @@ -758,7 +771,7 @@ define([ // Check modified section.textWithFrontMatter != newSection.textWithFrontMatter || // Check that section has not been detached or moved - section.elt.parentNode !== editor.contentElt || + section.elt.parentNode !== contentElt || // Check also the content since nodes can be injected in sections via copy/paste section.elt.textContent != newSection.textWithFrontMatter) { rightIndex = -index; @@ -789,30 +802,30 @@ define([ }); watcher.noWatch(function() { if(fileChanged === true) { - editor.contentElt.innerHTML = ''; - editor.contentElt.appendChild(newSectionEltList); + contentElt.innerHTML = ''; + contentElt.appendChild(newSectionEltList); setSelectionStartEnd(selectionStart, selectionEnd); } else { // Remove outdated sections sectionsToRemove.forEach(function(section) { // section can be already removed - section.elt.parentNode === editor.contentElt && editor.contentElt.removeChild(section.elt); + section.elt.parentNode === contentElt && contentElt.removeChild(section.elt); }); if(insertBeforeSection !== undefined) { - editor.contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt); + contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt); } else { - editor.contentElt.appendChild(newSectionEltList); + contentElt.appendChild(newSectionEltList); } // Remove unauthorized nodes (text nodes outside of sections or duplicated sections via copy/paste) - var childNode = editor.contentElt.firstChild; + var childNode = contentElt.firstChild; while(childNode) { var nextNode = childNode.nextSibling; if(!childNode.generated) { - editor.contentElt.removeChild(childNode); + contentElt.removeChild(childNode); } childNode = nextNode; } diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index b366cd91..9f2354d8 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -13,8 +13,8 @@ define([ var comments = new Extension("comments", 'Comments'); var commentTmpl = [ - '
', - '
<%= author %>
', + '
', + '
<%= author %>
', '
<%= content %>
', '
', ].join(''); @@ -23,7 +23,7 @@ define([ ' ', ' ', ' ', - ' <%- title %>', + ' “<%- title %>”', '', ].join(''); @@ -35,7 +35,7 @@ define([ var offsetMap = {}; function setCommentEltCoordinates(commentElt, y) { var lineIndex = Math.round(y / 10); - var yOffset = -8; + var yOffset = -10; if(commentElt.className.indexOf(' icon-fork') !== -1) { yOffset = -12; } @@ -48,9 +48,8 @@ define([ var inputElt; var marginElt; - var commentEltList = []; var newCommentElt = crel('a', { - class: 'discussion icon-comment new' + class: 'discussion icon-quote-left new' }); var cursorY; comments.onCursorCoordinates = function(x, y) { @@ -58,39 +57,56 @@ define([ setCommentEltCoordinates(newCommentElt, cursorY); }; + function Context(commentElt, fileDesc) { + this.commentElt = commentElt; + this.$commentElt = $(commentElt).addClass('active'); + this.fileDesc = fileDesc; + this.discussionIndex = commentElt.discussionIndex; + } + Context.prototype.getDiscussion = function() { + if(!this.discussionIndex) { + return this.fileDesc.newDiscussion; + } + return this.fileDesc.discussionList[this.discussionIndex]; + }; + Context.prototype.getPopoverElt = function() { + return document.querySelector('.comments-popover .popover:last-child'); + }; var currentContext; function movePopover(commentElt) { // Move popover in the margin - var context = currentContext; - context.popoverElt = document.querySelector('.comments-popover .popover:last-child'); + var popoverElt = currentContext.getPopoverElt(); var left = 0; - if(context.popoverElt.offsetWidth < marginElt.offsetWidth - 10) { - left = marginElt.offsetWidth - 10 - context.popoverElt.offsetWidth; + if(popoverElt.offsetWidth < marginElt.offsetWidth - 10) { + left = marginElt.offsetWidth - 10 - popoverElt.offsetWidth; } - context.popoverElt.style.left = left + 'px'; - context.popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(commentElt.style.right) - commentElt.offsetWidth / 2 - left) + 'px'; + popoverElt.style.left = left + 'px'; + popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(commentElt.style.right) - commentElt.offsetWidth / 2 - left) + 'px'; } var cssApplier; var currentFileDesc; var refreshTimeoutId; + var commentEltMap = {}; var refreshDiscussions = _.debounce(function() { if(currentFileDesc === undefined) { return; } var author = storage['author.name']; - commentEltList.forEach(function(commentElt) { - marginElt.removeChild(commentElt); - }); - commentEltList = []; offsetMap = {}; var discussionList = _.values(currentFileDesc.discussionList); function refreshOne() { if(discussionList.length === 0) { + // Remove outdated commentElt + _.filter(commentEltMap, function(commentElt, discussionIndex) { + return !_.has(currentFileDesc.discussionList, discussionIndex); + }).forEach(function(commentElt) { + marginElt.removeChild(commentElt); + }); // Move newCommentElt setCommentEltCoordinates(newCommentElt, cursorY); - if(currentContext && !currentContext.discussion.discussionIndex) { + if(currentContext && !currentContext.discussionIndex) { inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; movePopover(newCommentElt); } @@ -105,16 +121,19 @@ define([ } else { var isReplied = _.last(discussion.commentList).author != author; - commentElt.className += ' icon-comment' + (isReplied ? ' replied' : ' added'); + commentElt.className += ' icon-quote-left' + (isReplied ? ' replied' : ' added'); } commentElt.discussionIndex = discussion.discussionIndex; var coordinates = inputElt.getOffsetCoordinates(discussion.selectionEnd); var lineIndex = setCommentEltCoordinates(commentElt, coordinates.y); offsetMap[lineIndex] = (offsetMap[lineIndex] || 0) + 1; - marginElt.appendChild(commentElt); - commentEltList.push(commentElt); - if(currentContext && currentContext.discussion == discussion) { + var oldCommentElt = commentEltMap[discussion.discussionIndex]; + oldCommentElt && marginElt.removeChild(oldCommentElt); + marginElt.appendChild(commentElt); + commentEltMap[discussion.discussionIndex] = commentElt; + + if(currentContext && currentContext.getDiscussion() == discussion) { inputElt.scrollTop += parseInt(commentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; movePopover(commentElt); } @@ -140,15 +159,15 @@ define([ if(currentContext !== undefined) { // Refresh conversation if popover is open var context = currentContext; - if(context.discussion.discussionIndex) { - context.discussion = currentFileDesc.discussionList[context.discussion.discussionIndex]; + if(context.discussionIndex) { context.popoverElt.querySelector('.discussion-comment-list').innerHTML = getDiscussionComments(); } try { cssApplier.undoToRange(context.rangyRange); } catch(e) {} - context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd); + var discussion = context.getDiscussion(); + context.selectionRange = inputElt.createRange(discussion.selectionStart, discussion.selectionEnd); // Highlight selected text context.rangyRange = rangy.createRange(); @@ -174,7 +193,7 @@ define([ comments.onDiscussionRemoved = function(fileDesc, discussion) { if(currentFileDesc === fileDesc) { // Close popover if the discussion has been removed - if(currentContext !== undefined && currentContext.discussion.discussionIndex == discussion.discussionIndex) { + if(currentContext !== undefined && currentContext.discussionIndex == discussion.discussionIndex) { closeCurrentPopover(); } refreshDiscussions(); @@ -186,13 +205,17 @@ define([ }; function getDiscussionComments() { - if(currentContext.discussion.type == 'conflict') { + var discussion = currentContext.getDiscussion(); + var author = storage['author.name']; + if(discussion.type == 'conflict') { return ''; } - return currentContext.discussion.commentList.map(function(comment) { + return discussion.commentList.map(function(comment) { + var commentAuthor = comment.author || 'Anonymous'; return _.template(commentTmpl, { - author: comment.author || 'Anonymous', - content: comment.content + author: commentAuthor, + content: comment.content, + reply: comment.author != author }); }).join(''); } @@ -221,37 +244,36 @@ define([ if(!currentContext) { return true; } - var titleLength = currentContext.discussion.selectionEnd - currentContext.discussion.selectionStart; - var title = inputElt.textContent.substr(currentContext.discussion.selectionStart, titleLength > 20 ? 20 : titleLength); + var discussion = currentContext.getDiscussion(); + var titleLength = discussion.selectionEnd - discussion.selectionStart; + var title = inputElt.textContent.substr(discussion.selectionStart, titleLength > 20 ? 20 : titleLength); if(titleLength > 20) { title += '...'; } return _.template(popoverTitleTmpl, { title: title, - type: currentContext.discussion.type + type: discussion.type }); }, content: function() { var content = _.template(commentsPopoverContentHTML, { commentList: getDiscussionComments(), - type: currentContext.discussion.type + type: currentContext.getDiscussion().type }); return content; }, selector: '#wmd-input > .editor-margin > .discussion' }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { closeCurrentPopover(); - var context = { - $commentElt: $(evt.target).addClass('active'), - fileDesc: currentFileDesc - }; + var context = new Context(evt.target, currentFileDesc); currentContext = context; inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; // If it's an existing discussion - if(evt.target.discussionIndex) { - context.discussion = currentFileDesc.discussionList[evt.target.discussionIndex]; - context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd); + var discussion = context.getDiscussion(); + if(discussion) { + context.selectionRange = inputElt.createRange(discussion.selectionStart, discussion.selectionEnd); + inputElt.setSelectionStartEnd(discussion.selectionStart, discussion.selectionEnd, false); return; } @@ -272,24 +294,23 @@ define([ } } context.selectionRange = inputElt.createRange(selectionStart, selectionEnd); - context.discussion = { + currentFileDesc.newDiscussion = { selectionStart: selectionStart, selectionEnd: selectionEnd, commentList: [] }; - currentFileDesc.newDiscussion = context.discussion; }).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) { var context = currentContext; - context.popoverElt = document.querySelector('.comments-popover .popover:last-child'); - movePopover(evt.target); + movePopover(context.commentElt); + var popoverElt = context.getPopoverElt(); // Scroll to the bottom of the discussion - context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; + popoverElt.querySelector('.popover-content').scrollTop = 9999999; - context.$authorInputElt = $(context.popoverElt.querySelector('.input-comment-author')).val(storage['author.name']); - context.$contentInputElt = $(context.popoverElt.querySelector('.input-comment-content')); - var $addButton = $(context.popoverElt.querySelector('.action-add-comment')); - context.$contentInputElt.keydown(function(evt) { + context.$authorInputElt = $(popoverElt.querySelector('.input-comment-author')).val(storage['author.name']); + context.$contentInputElt = $(popoverElt.querySelector('.input-comment-content')); + var $addButton = $(popoverElt.querySelector('.action-add-comment')); + $().add(context.$contentInputElt).add(context.$authorInputElt).keydown(function(evt) { // Enter key switch(evt.which) { case 13: @@ -310,24 +331,25 @@ define([ return; } + var discussion = context.getDiscussion(); context.$contentInputElt.val(''); closeCurrentPopover(); - context.discussion.commentList.push({ + discussion.commentList.push({ author: author, content: content }); var discussionList = context.fileDesc.discussionList || {}; - if(!context.discussion.discussionIndex) { + if(!discussion.discussionIndex) { // Create discussion index var discussionIndex; do { discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision } while(_.has(discussionList, discussionIndex)); - context.discussion.discussionIndex = discussionIndex; - discussionList[discussionIndex] = context.discussion; + discussion.discussionIndex = discussionIndex; + discussionList[discussionIndex] = discussion; context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage - eventMgr.onDiscussionCreated(context.fileDesc, context.discussion); + eventMgr.onDiscussionCreated(context.fileDesc, discussion); } else { context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage @@ -336,33 +358,28 @@ define([ inputElt.focus(); }); - var $removeButton = $(context.popoverElt.querySelector('.action-remove-discussion')); + var $removeButton = $(popoverElt.querySelector('.action-remove-discussion')); if(evt.target.discussionIndex) { // If it's an existing discussion - /* - 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')); + var $removeCancelButton = $(popoverElt.querySelectorAll('.action-remove-discussion-cancel')); + var $removeConfirmButton = $(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'); - context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; + $(popoverElt.querySelector('.new-comment-block')).addClass('hide'); + $(popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide'); + popoverElt.querySelector('.popover-content').scrollTop = 9999999; }); $removeCancelButton.click(function() { - $(context.popoverElt.querySelector('.new-comment-block')).removeClass('hide'); - $(context.popoverElt.querySelector('.remove-discussion-confirm')).addClass('hide'); - context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; + $(popoverElt.querySelector('.new-comment-block')).removeClass('hide'); + $(popoverElt.querySelector('.remove-discussion-confirm')).addClass('hide'); + popoverElt.querySelector('.popover-content').scrollTop = 9999999; context.$contentInputElt.focus(); }); $removeConfirmButton.click(function() { closeCurrentPopover(); - delete context.fileDesc.discussionList[context.discussion.discussionIndex]; + var discussion = context.getDiscussion(); + delete context.fileDesc.discussionList[discussion.discussionIndex]; context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage - eventMgr.onCommentsChanged(context.fileDesc); + eventMgr.onDiscussionRemoved(context.fileDesc, discussion); inputElt.focus(); }); } @@ -372,7 +389,7 @@ define([ } // Prevent from closing on click inside the popover - $(context.popoverElt).on('click', function(evt) { + $(popoverElt).on('click', function(evt) { evt.stopPropagation(); }); diff --git a/public/res/html/commentsPopoverContent.html b/public/res/html/commentsPopoverContent.html index 0e8fc5b0..24fc0d95 100644 --- a/public/res/html/commentsPopoverContent.html +++ b/public/res/html/commentsPopoverContent.html @@ -1,7 +1,7 @@
<%= commentList %>
- +
diff --git a/public/res/providers/dropboxProvider.js b/public/res/providers/dropboxProvider.js index 1e704bb6..48819412 100644 --- a/public/res/providers/dropboxProvider.js +++ b/public/res/providers/dropboxProvider.js @@ -129,6 +129,7 @@ define([ syncAttributes.discussionList = discussionList; } syncAttributes.contentCRC = contentCRC; + syncAttributes.titleCRC = titleCRC; // Not synchronized but has to be there for syncMerge syncAttributes.discussionListCRC = discussionListCRC; callback(undefined, true); diff --git a/public/res/styles/main.less b/public/res/styles/main.less index 6f100a38..5b6afe0f 100644 --- a/public/res/styles/main.less +++ b/public/res/styles/main.less @@ -94,7 +94,7 @@ @list-group-active-border: fade(@secondary, 5%); @list-group-hover-bg: @btn-default-hover-bg; @list-group-hover-border-color: fade(@secondary, 10%); -@input-color: @secondary-color-dark; +@input-color: @secondary-color-darkest; @input-color-placeholder: @disabled-color; @btn-default-color: @secondary-color-darker; @btn-default-bg: @transparent; @@ -1059,6 +1059,9 @@ a { top: 0; .discussion { font-size: 18px; + &:before { + margin-right: 0; + } &.new { color: fade(@tertiary-color, 10%); &:hover, &.active, &.active:hover { @@ -1072,7 +1075,7 @@ a { } } &.replied { - color: fade(@label-danger-bg, 45%); + color: fade(@label-danger-bg, 55%); &:hover, &.active, &.active:hover { color: fade(@label-danger-bg, 80%) !important; } @@ -1083,9 +1086,6 @@ a { &:hover, &.active, &.active:hover { color: @label-danger-bg !important; } - &:before { - margin-right: 0; - } } position: absolute; cursor: pointer; @@ -1094,7 +1094,7 @@ a { } } } - &.has-selection > .editor-margin .icon-comment.new { + &.has-selection > .editor-margin .discussion.new { color: fade(@tertiary-color, 25%); } @@ -1423,6 +1423,7 @@ input[type="file"] { padding: 5px 0 15px; border-bottom: 1px solid @hr-border; line-height: @headings-line-height; + overflow: hidden; .action-remove-discussion { font-size: 16px; line-height: 22px; @@ -1447,15 +1448,25 @@ input[type="file"] { margin-bottom: 5px; } .comment-author { - padding-left: 12px; font-weight: bold; + color: @input-color; + } + .icon-comment { + font-size: 14px; + color: fade(@label-warning-bg, 60%); + } + .reply .icon-comment { + color: fade(@label-danger-bg, 70%); } .input-comment-author { border: none; background: none; .box-shadow(none); font-weight: bold; - height: 32px; + height: 28px; + padding: 0 0 5px; + width: 150px; + display: inline-block; } }