From 7f9747ef6cc42a2b094baa64ead6321e49a29520 Mon Sep 17 00:00:00 2001 From: benweet Date: Tue, 25 Mar 2014 00:23:42 +0000 Subject: [PATCH] Added diff-match-patch --- bower.json | 3 +- public/res/editor.js | 165 ++++++++++-------- public/res/extensions/comments.js | 44 ++--- public/res/main.js | 11 +- public/res/providers/gdriveProviderBuilder.js | 12 -- public/res/styles/main.less | 16 +- 6 files changed, 134 insertions(+), 117 deletions(-) diff --git a/bower.json b/bower.json index 14a1176e..4facd562 100644 --- a/bower.json +++ b/bower.json @@ -26,6 +26,7 @@ "stackedit-pagedown": "https://github.com/benweet/stackedit-pagedown.git#d81b3689a99c84832d8885f607e6de860fb7d94a", "prism": "gh-pages", "MutationObservers": "https://github.com/Polymer/MutationObservers.git#~0.2.1", - "rangy": "~1.2.3" + "rangy": "~1.2.3", + "google-diff-match-patch-js": "~1.0.0" } } diff --git a/public/res/editor.js b/public/res/editor.js index 0524d9a0..3a7acb42 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -5,10 +5,12 @@ define([ 'settings', 'eventMgr', 'prism-core', + 'diff_match_patch_uncompressed', 'crel', 'MutationObservers', 'libs/prism-markdown' -], function ($, _, settings, eventMgr, Prism, crel) { +], function ($, _, settings, eventMgr, Prism, diff_match_patch, crel) { + var diffMatchPatch = new diff_match_patch(); function strSplice(str, i, remove, add) { remove = +remove || 0; @@ -61,6 +63,35 @@ define([ fileDesc = selectedFileDesc; }); + var previousTextContent; + var undoManager = {}; + (function() { + var undoStack = []; // A stack of undo states + var stackPtr = 0; // The index of the current state + var lastTime; + function saveState(mode) { + var currentTime = Date.now(); + var newState = { + selectionStart: selectionStart, + selectionEnd: selectionEnd, + scrollTop: scrollTop, + mode: mode + } + if(!stackPtr && (currentTime - lastTime > 1000 || mode != undoStack[stackPtr - 1].mode)) { + undoStack.push(lastState); + stackPtr++; + } + if(mode != 'move') { + lastState.content = previousTextContent; + lastState.discussionList = $.extend(true, {}, fileDesc.discussionList); + } + } + undoManager.setCommandMode = function() { + saveState('command'); + }; + + }); + function saveSelectionState() { var selection = window.getSelection(); if (selection.rangeCount > 0) { @@ -89,36 +120,6 @@ define([ } } - var previousTextContent; - function getContentChange(textContent) { - // Find the first modified char - var startIndex = 0; - var startIndexMax = Math.min(previousTextContent.length, textContent.length); - while (startIndex < startIndexMax) { - if (previousTextContent.charCodeAt(startIndex) !== textContent.charCodeAt(startIndex)) { - break; - } - startIndex++; - } - // Find the last modified char - var endIndex = 1; - var endIndexMax = Math.min(previousTextContent.length - startIndex, textContent.length - startIndex); - while (endIndex <= endIndexMax) { - if (previousTextContent.charCodeAt(previousTextContent.length - endIndex) !== textContent.charCodeAt(textContent.length - endIndex)) { - break; - } - endIndex++; - } - - var replacement = textContent.substring(startIndex, textContent.length - endIndex + 1); - endIndex = previousTextContent.length - endIndex + 1; - return { - startIndex: startIndex, - endIndex: endIndex, - replacement: replacement - }; - } - function checkContentChange() { saveSelectionState(); var currentTextContent = inputElt.textContent; @@ -129,33 +130,44 @@ define([ if(!/\n$/.test(currentTextContent)) { currentTextContent += '\n'; } - var change = getContentChange(currentTextContent); - var endOffset = change.startIndex + change.replacement.length - change.endIndex; - - // Move comments according to change + var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent); + // Move comments according to changes var updateDiscussionList = false; - _.each(fileDesc.discussionList, function(discussion) { - if(discussion.isRemoved === true) { + var startOffset = 0; + changes.forEach(function(change) { + var changeType = change[0]; + var changeText = change[1]; + if(changeType === 0) { + startOffset += changeText.length; return; } - // selectionEnd - if(discussion.selectionEnd >= change.endIndex) { - discussion.selectionEnd += endOffset; - updateDiscussionList = true; - } - else if(discussion.selectionEnd > change.startIndex) { - discussion.selectionEnd = change.startIndex; - updateDiscussionList = true; - } - // selectionStart - if(discussion.selectionStart >= change.endIndex) { - discussion.selectionStart += endOffset; - updateDiscussionList = true; - } - else if(discussion.selectionStart > change.startIndex) { - discussion.selectionStart = change.startIndex; - updateDiscussionList = true; + var endOffset = startOffset; + var diffOffset = changeText.length; + if(changeType === -1) { + endOffset += diffOffset; + diffOffset = -diffOffset; } + _.each(fileDesc.discussionList, function(discussion) { + // selectionEnd + if(discussion.selectionEnd >= endOffset) { + discussion.selectionEnd += diffOffset; + updateDiscussionList = true; + } + else if(discussion.selectionEnd > startOffset) { + discussion.selectionEnd = startOffset; + updateDiscussionList = true; + } + // selectionStart + if(discussion.selectionStart >= endOffset) { + discussion.selectionStart += diffOffset; + updateDiscussionList = true; + } + else if(discussion.selectionStart > startOffset) { + discussion.selectionStart = startOffset; + updateDiscussionList = true; + } + }); + startOffset = endOffset; }); if(updateDiscussionList === true) { fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage @@ -329,6 +341,7 @@ define([ }, 0); eventMgr.addListener('onLayoutResize', adjustCursorPosition); + var contentObserver; editor.init = function(elt1, elt2) { inputElt = elt1; $inputElt = $(inputElt); @@ -347,8 +360,8 @@ define([ inputElt.appendChild(editor.marginElt); editor.$marginElt = $(editor.marginElt); - var observer = new MutationObserver(checkContentChange); - observer.observe(editor.contentElt, { + contentObserver = new MutationObserver(checkContentChange); + contentObserver.observe(editor.contentElt, { childList: true, subtree: true, characterData: true @@ -383,10 +396,16 @@ define([ return this.textContent; }, set: function (value) { - var contentChange = getContentChange(value); - var range = inputElt.createRange(contentChange.startIndex, contentChange.endIndex); + 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(contentChange.replacement)); + range.insertNode(document.createTextNode(replacement)); } }); @@ -599,10 +618,10 @@ define([ if(index >= newSectionList.length || // Check modified section.textWithFrontMatter != newSection.textWithFrontMatter || - // Check that section has not been detached from the DOM with backspace - !section.highlightedContent.parentNode || + // Check that section has not been detached or moved + section.elt.parentNode !== editor.contentElt || // Check also the content since nodes can be injected in sections via copy/paste - section.highlightedContent.textContent != newSection.textWithFrontMatter) { + section.elt.textContent != newSection.textWithFrontMatter) { leftIndex = index; return true; } @@ -615,10 +634,10 @@ define([ if(index >= newSectionList.length || // Check modified section.textWithFrontMatter != newSection.textWithFrontMatter || - // Check that section has not been detached from the DOM with backspace - !section.highlightedContent.parentNode || + // Check that section has not been detached or moved + section.elt.parentNode !== editor.contentElt || // Check also the content since nodes can be injected in sections via copy/paste - section.highlightedContent.textContent != newSection.textWithFrontMatter) { + section.elt.textContent != newSection.textWithFrontMatter) { rightIndex = -index; return true; } @@ -643,8 +662,9 @@ define([ var newSectionEltList = document.createDocumentFragment(); modifiedSections.forEach(function(section) { highlight(section); - newSectionEltList.appendChild(section.highlightedContent); + newSectionEltList.appendChild(section.elt); }); + contentObserver.disconnect(); if(fileChanged === true) { editor.contentElt.innerHTML = ''; editor.contentElt.appendChild(newSectionEltList); @@ -653,14 +673,12 @@ define([ else { // Remove outdated sections sectionsToRemove.forEach(function(section) { - var sectionElt = document.getElementById("wmd-input-section-" + section.id); // section can be already removed - sectionElt && editor.contentElt.removeChild(sectionElt); + section.elt.parentNode === editor.contentElt && editor.contentElt.removeChild(section.elt); }); if(insertBeforeSection !== undefined) { - var insertBeforeElt = document.getElementById("wmd-input-section-" + insertBeforeSection.id); - editor.contentElt.insertBefore(newSectionEltList, insertBeforeElt); + editor.contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt); } else { editor.contentElt.appendChild(newSectionEltList); @@ -678,6 +696,11 @@ define([ inputElt.setSelectionStartEnd(selectionStart, selectionEnd); } + contentObserver.observe(editor.contentElt, { + childList: true, + subtree: true, + characterData: true + }); } function highlight(section) { @@ -696,7 +719,7 @@ define([ }); sectionElt.generated = true; sectionElt.innerHTML = text; - section.highlightedContent = sectionElt; + section.elt = sectionElt; } return editor; diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index 08d7e070..8121f7ff 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -18,6 +18,14 @@ define([ '
<%= content %>
', '', ].join(''); + var popoverTitleTmpl = [ + '', + ' ', + ' ', + ' ', + ' <%- title %>', + '', + ].join(''); var eventMgr; comments.onEventMgrCreated = function(eventMgrParam) { @@ -61,13 +69,10 @@ define([ offsetMap = {}; var discussionList = _.map(currentFileDesc.discussionList, _.identity); function refreshOne() { - var discussion; - do { - if(discussionList.length === 0) { - return; - } - discussion = discussionList.pop(); - } while(discussion.isRemoved); + if(discussionList.length === 0) { + return; + } + var discussion = discussionList.pop(); var commentElt = crel('a', { class: 'icon-comment' }); @@ -159,8 +164,9 @@ define([ if(titleLength > 20) { title += '...'; } - title = title.replace(/&/g, '&').replace(/' + title; + return _.template(popoverTitleTmpl, { + title: title + }); }, content: function() { var content = _.template(commentsPopoverContentHTML, { @@ -172,7 +178,8 @@ define([ }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { closeCurrentPopover(); var context = { - $commentElt: $(evt.target).addClass('active') + $commentElt: $(evt.target).addClass('active'), + fileDesc: currentFileDesc }; currentContext = context; inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; @@ -247,7 +254,7 @@ define([ context.$contentInputElt.val(''); closeCurrentPopover(); - var discussionList = currentFileDesc.discussionList || {}; + var discussionList = context.fileDesc.discussionList || {}; var isNew = false; if(!context.discussion.discussionIndex) { isNew = true; @@ -263,10 +270,10 @@ define([ author: author, content: content }); - currentFileDesc.discussionList = discussionList; // Write discussionList in localStorage + context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage isNew ? - eventMgr.onDiscussionCreated(currentFileDesc, context.discussion) : - eventMgr.onCommentAdded(currentFileDesc, context.discussion); + eventMgr.onDiscussionCreated(context.fileDesc, context.discussion) : + eventMgr.onCommentAdded(context.fileDesc, context.discussion); inputElt.focus(); }); @@ -288,12 +295,9 @@ define([ }); $removeConfirmButton.click(function() { closeCurrentPopover(); - context.discussion.isRemoved = true; - delete context.discussion.selectionStart; - delete context.discussion.selectionEnd; - delete context.discussion.commentList; - currentFileDesc.discussionList = currentFileDesc.discussionList; // Write discussionList in localStorage - eventMgr.onDiscussionRemoved(currentFileDesc, context.discussion); + delete context.fileDesc.discussionList[context.discussion.discussionIndex]; + context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage + eventMgr.onDiscussionRemoved(context.fileDesc, context.discussion); inputElt.focus(); }); } diff --git a/public/res/main.js b/public/res/main.js index 1de71861..c130a555 100644 --- a/public/res/main.js +++ b/public/res/main.js @@ -42,7 +42,6 @@ requirejs.config({ 'requirejs-text': 'bower-libs/requirejs-text/text', 'bootstrap-tour': 'bower-libs/bootstrap-tour/build/js/bootstrap-tour', css_browser_selector: 'bower-libs/css_browser_selector/css_browser_selector', - 'jquery-mousewheel': 'bower-libs/jquery-mousewheel/jquery.mousewheel', 'pagedown-extra': 'bower-libs/pagedown-extra/Markdown.Extra', pagedown: 'bower-libs/stackedit-pagedown/Markdown.Editor', 'require-css': 'bower-libs/require-css/css', @@ -58,7 +57,9 @@ requirejs.config({ MutationObservers: 'bower-libs/MutationObservers/MutationObserver', WeakMap: 'bower-libs/WeakMap/weakmap', rangy: 'bower-libs/rangy/rangy-core', - 'rangy-cssclassapplier': 'bower-libs/rangy/rangy-cssclassapplier' + 'rangy-cssclassapplier': 'bower-libs/rangy/rangy-cssclassapplier', + diff_match_patch: 'bower-libs/google-diff-match-patch-js/diff_match_patch', + diff_match_patch_uncompressed: 'bower-libs/google-diff-match-patch-js/diff_match_patch_uncompressed' }, shim: { underscore: { @@ -73,6 +74,9 @@ requirejs.config({ ], exports: 'jQuery.jGrowl' }, + diff_match_patch_uncompressed: { + exports: 'diff_match_patch' + }, rangy: { exports: 'rangy' }, @@ -131,9 +135,6 @@ requirejs.config({ 'jquery-waitforimages': [ 'jquery' ], - 'jquery-mousewheel': [ - 'jquery' - ], uilayout: [ 'jquery-ui-effect-slide' ], diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js index d40537c7..7388c3b0 100644 --- a/public/res/providers/gdriveProviderBuilder.js +++ b/public/res/providers/gdriveProviderBuilder.js @@ -451,18 +451,6 @@ define([ }; function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) { - if(localDiscussion.isRemoved === true) { - realtimeDiscussion.set('isRemoved'); - realtimeDiscussion.delete('selectionStart'); - realtimeDiscussion.delete('selectionEnd'); - return realtimeDiscussion.delete('commentList'); - } - if(realtimeDiscussion.get('isRemoved') === true) { - localDiscussion.isRemoved = true; - delete localDiscussion.selectionStart; - delete localDiscussion.selectionEnd; - return delete localDiscussion.commentList; - } if(takeServer) { localDiscussion.selectionStart = realtimeDiscussion.get('selectionStart'); localDiscussion.selectionEnd = realtimeDiscussion.get('selectionEnd'); diff --git a/public/res/styles/main.less b/public/res/styles/main.less index b1d1bf6e..7adcd357 100644 --- a/public/res/styles/main.less +++ b/public/res/styles/main.less @@ -127,8 +127,8 @@ @popover-arrow-outer-color: @secondary-border-color; @popover-title-bg: @transparent; @alert-border-radius: 0; -@label-warning-bg: #da0; -@label-danger-bg: #d00; +@label-warning-bg: darken(@logo-yellow, 4%); +@label-danger-bg: darken(@logo-orange, 4%); body { @@ -154,8 +154,8 @@ body { .user-select(none); } -.dropdown-menu, .modal-content, .panel-content, .search-bar .popover { - .box-shadow(0 4px 12px rgba(0,0,0,.125)); +.dropdown-menu, .modal-content, .panel-content, .search-bar, .popover { + .box-shadow(0 4px 16px rgba(0,0,0,.225)); } .dropdown-menu { @@ -1066,15 +1066,15 @@ a { } } &.replied { - color: fade(@label-danger-bg, 35%); + color: fade(@label-danger-bg, 45%); &:hover, &.active, &.active:hover { - color: fade(@label-danger-bg, 45%) !important; + color: fade(@label-danger-bg, 80%) !important; } } &.added { - color: fade(@label-warning-bg, 40%); + color: fade(@label-warning-bg, 45%); &:hover, &.active, &.active:hover { - color: fade(@label-warning-bg, 60%) !important; + color: fade(@label-warning-bg, 80%) !important; } } .transition(~"color ease-in-out .25s");