Added diff-match-patch

This commit is contained in:
benweet 2014-03-25 00:23:42 +00:00
parent 8b565e32ce
commit 7f9747ef6c
6 changed files with 134 additions and 117 deletions

View File

@ -26,6 +26,7 @@
"stackedit-pagedown": "https://github.com/benweet/stackedit-pagedown.git#d81b3689a99c84832d8885f607e6de860fb7d94a", "stackedit-pagedown": "https://github.com/benweet/stackedit-pagedown.git#d81b3689a99c84832d8885f607e6de860fb7d94a",
"prism": "gh-pages", "prism": "gh-pages",
"MutationObservers": "https://github.com/Polymer/MutationObservers.git#~0.2.1", "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"
} }
} }

View File

@ -5,10 +5,12 @@ define([
'settings', 'settings',
'eventMgr', 'eventMgr',
'prism-core', 'prism-core',
'diff_match_patch_uncompressed',
'crel', 'crel',
'MutationObservers', 'MutationObservers',
'libs/prism-markdown' '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) { function strSplice(str, i, remove, add) {
remove = +remove || 0; remove = +remove || 0;
@ -61,6 +63,35 @@ define([
fileDesc = selectedFileDesc; 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() { function saveSelectionState() {
var selection = window.getSelection(); var selection = window.getSelection();
if (selection.rangeCount > 0) { 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() { function checkContentChange() {
saveSelectionState(); saveSelectionState();
var currentTextContent = inputElt.textContent; var currentTextContent = inputElt.textContent;
@ -129,33 +130,44 @@ define([
if(!/\n$/.test(currentTextContent)) { if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n'; currentTextContent += '\n';
} }
var change = getContentChange(currentTextContent); var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent);
var endOffset = change.startIndex + change.replacement.length - change.endIndex; // Move comments according to changes
// Move comments according to change
var updateDiscussionList = false; var updateDiscussionList = false;
_.each(fileDesc.discussionList, function(discussion) { var startOffset = 0;
if(discussion.isRemoved === true) { changes.forEach(function(change) {
var changeType = change[0];
var changeText = change[1];
if(changeType === 0) {
startOffset += changeText.length;
return; return;
} }
// selectionEnd var endOffset = startOffset;
if(discussion.selectionEnd >= change.endIndex) { var diffOffset = changeText.length;
discussion.selectionEnd += endOffset; if(changeType === -1) {
updateDiscussionList = true; endOffset += diffOffset;
} diffOffset = -diffOffset;
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;
} }
_.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) { if(updateDiscussionList === true) {
fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage
@ -329,6 +341,7 @@ define([
}, 0); }, 0);
eventMgr.addListener('onLayoutResize', adjustCursorPosition); eventMgr.addListener('onLayoutResize', adjustCursorPosition);
var contentObserver;
editor.init = function(elt1, elt2) { editor.init = function(elt1, elt2) {
inputElt = elt1; inputElt = elt1;
$inputElt = $(inputElt); $inputElt = $(inputElt);
@ -347,8 +360,8 @@ define([
inputElt.appendChild(editor.marginElt); inputElt.appendChild(editor.marginElt);
editor.$marginElt = $(editor.marginElt); editor.$marginElt = $(editor.marginElt);
var observer = new MutationObserver(checkContentChange); contentObserver = new MutationObserver(checkContentChange);
observer.observe(editor.contentElt, { contentObserver.observe(editor.contentElt, {
childList: true, childList: true,
subtree: true, subtree: true,
characterData: true characterData: true
@ -383,10 +396,16 @@ define([
return this.textContent; return this.textContent;
}, },
set: function (value) { set: function (value) {
var contentChange = getContentChange(value); var startOffset = diffMatchPatch.diff_commonPrefix(previousTextContent, value);
var range = inputElt.createRange(contentChange.startIndex, contentChange.endIndex); 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.deleteContents();
range.insertNode(document.createTextNode(contentChange.replacement)); range.insertNode(document.createTextNode(replacement));
} }
}); });
@ -599,10 +618,10 @@ define([
if(index >= newSectionList.length || if(index >= newSectionList.length ||
// Check modified // Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter || section.textWithFrontMatter != newSection.textWithFrontMatter ||
// Check that section has not been detached from the DOM with backspace // Check that section has not been detached or moved
!section.highlightedContent.parentNode || section.elt.parentNode !== editor.contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste // 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; leftIndex = index;
return true; return true;
} }
@ -615,10 +634,10 @@ define([
if(index >= newSectionList.length || if(index >= newSectionList.length ||
// Check modified // Check modified
section.textWithFrontMatter != newSection.textWithFrontMatter || section.textWithFrontMatter != newSection.textWithFrontMatter ||
// Check that section has not been detached from the DOM with backspace // Check that section has not been detached or moved
!section.highlightedContent.parentNode || section.elt.parentNode !== editor.contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste // 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; rightIndex = -index;
return true; return true;
} }
@ -643,8 +662,9 @@ define([
var newSectionEltList = document.createDocumentFragment(); var newSectionEltList = document.createDocumentFragment();
modifiedSections.forEach(function(section) { modifiedSections.forEach(function(section) {
highlight(section); highlight(section);
newSectionEltList.appendChild(section.highlightedContent); newSectionEltList.appendChild(section.elt);
}); });
contentObserver.disconnect();
if(fileChanged === true) { if(fileChanged === true) {
editor.contentElt.innerHTML = ''; editor.contentElt.innerHTML = '';
editor.contentElt.appendChild(newSectionEltList); editor.contentElt.appendChild(newSectionEltList);
@ -653,14 +673,12 @@ define([
else { else {
// Remove outdated sections // Remove outdated sections
sectionsToRemove.forEach(function(section) { sectionsToRemove.forEach(function(section) {
var sectionElt = document.getElementById("wmd-input-section-" + section.id);
// section can be already removed // section can be already removed
sectionElt && editor.contentElt.removeChild(sectionElt); section.elt.parentNode === editor.contentElt && editor.contentElt.removeChild(section.elt);
}); });
if(insertBeforeSection !== undefined) { if(insertBeforeSection !== undefined) {
var insertBeforeElt = document.getElementById("wmd-input-section-" + insertBeforeSection.id); editor.contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt);
editor.contentElt.insertBefore(newSectionEltList, insertBeforeElt);
} }
else { else {
editor.contentElt.appendChild(newSectionEltList); editor.contentElt.appendChild(newSectionEltList);
@ -678,6 +696,11 @@ define([
inputElt.setSelectionStartEnd(selectionStart, selectionEnd); inputElt.setSelectionStartEnd(selectionStart, selectionEnd);
} }
contentObserver.observe(editor.contentElt, {
childList: true,
subtree: true,
characterData: true
});
} }
function highlight(section) { function highlight(section) {
@ -696,7 +719,7 @@ define([
}); });
sectionElt.generated = true; sectionElt.generated = true;
sectionElt.innerHTML = text; sectionElt.innerHTML = text;
section.highlightedContent = sectionElt; section.elt = sectionElt;
} }
return editor; return editor;

View File

@ -18,6 +18,14 @@ define([
' <div class="comment-content"><%= content %></div>', ' <div class="comment-content"><%= content %></div>',
'</div>', '</div>',
].join(''); ].join('');
var popoverTitleTmpl = [
'<span class="clearfix">',
' <a href="#" class="action-remove-discussion pull-right">',
' <i class="icon-trash"></i>',
' </a>',
' <%- title %>',
'</span>',
].join('');
var eventMgr; var eventMgr;
comments.onEventMgrCreated = function(eventMgrParam) { comments.onEventMgrCreated = function(eventMgrParam) {
@ -61,13 +69,10 @@ define([
offsetMap = {}; offsetMap = {};
var discussionList = _.map(currentFileDesc.discussionList, _.identity); var discussionList = _.map(currentFileDesc.discussionList, _.identity);
function refreshOne() { function refreshOne() {
var discussion; if(discussionList.length === 0) {
do { return;
if(discussionList.length === 0) { }
return; var discussion = discussionList.pop();
}
discussion = discussionList.pop();
} while(discussion.isRemoved);
var commentElt = crel('a', { var commentElt = crel('a', {
class: 'icon-comment' class: 'icon-comment'
}); });
@ -159,8 +164,9 @@ define([
if(titleLength > 20) { if(titleLength > 20) {
title += '...'; title += '...';
} }
title = title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' '); return _.template(popoverTitleTmpl, {
return '<a href="#" class="action-remove-discussion pull-right"><i class="icon-trash"></i></a>' + title; title: title
});
}, },
content: function() { content: function() {
var content = _.template(commentsPopoverContentHTML, { var content = _.template(commentsPopoverContentHTML, {
@ -172,7 +178,8 @@ define([
}).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) {
closeCurrentPopover(); closeCurrentPopover();
var context = { var context = {
$commentElt: $(evt.target).addClass('active') $commentElt: $(evt.target).addClass('active'),
fileDesc: currentFileDesc
}; };
currentContext = context; currentContext = context;
inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
@ -247,7 +254,7 @@ define([
context.$contentInputElt.val(''); context.$contentInputElt.val('');
closeCurrentPopover(); closeCurrentPopover();
var discussionList = currentFileDesc.discussionList || {}; var discussionList = context.fileDesc.discussionList || {};
var isNew = false; var isNew = false;
if(!context.discussion.discussionIndex) { if(!context.discussion.discussionIndex) {
isNew = true; isNew = true;
@ -263,10 +270,10 @@ define([
author: author, author: author,
content: content content: content
}); });
currentFileDesc.discussionList = discussionList; // Write discussionList in localStorage context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
isNew ? isNew ?
eventMgr.onDiscussionCreated(currentFileDesc, context.discussion) : eventMgr.onDiscussionCreated(context.fileDesc, context.discussion) :
eventMgr.onCommentAdded(currentFileDesc, context.discussion); eventMgr.onCommentAdded(context.fileDesc, context.discussion);
inputElt.focus(); inputElt.focus();
}); });
@ -288,12 +295,9 @@ define([
}); });
$removeConfirmButton.click(function() { $removeConfirmButton.click(function() {
closeCurrentPopover(); closeCurrentPopover();
context.discussion.isRemoved = true; delete context.fileDesc.discussionList[context.discussion.discussionIndex];
delete context.discussion.selectionStart; context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage
delete context.discussion.selectionEnd; eventMgr.onDiscussionRemoved(context.fileDesc, context.discussion);
delete context.discussion.commentList;
currentFileDesc.discussionList = currentFileDesc.discussionList; // Write discussionList in localStorage
eventMgr.onDiscussionRemoved(currentFileDesc, context.discussion);
inputElt.focus(); inputElt.focus();
}); });
} }

View File

@ -42,7 +42,6 @@ requirejs.config({
'requirejs-text': 'bower-libs/requirejs-text/text', 'requirejs-text': 'bower-libs/requirejs-text/text',
'bootstrap-tour': 'bower-libs/bootstrap-tour/build/js/bootstrap-tour', 'bootstrap-tour': 'bower-libs/bootstrap-tour/build/js/bootstrap-tour',
css_browser_selector: 'bower-libs/css_browser_selector/css_browser_selector', 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-extra': 'bower-libs/pagedown-extra/Markdown.Extra',
pagedown: 'bower-libs/stackedit-pagedown/Markdown.Editor', pagedown: 'bower-libs/stackedit-pagedown/Markdown.Editor',
'require-css': 'bower-libs/require-css/css', 'require-css': 'bower-libs/require-css/css',
@ -58,7 +57,9 @@ requirejs.config({
MutationObservers: 'bower-libs/MutationObservers/MutationObserver', MutationObservers: 'bower-libs/MutationObservers/MutationObserver',
WeakMap: 'bower-libs/WeakMap/weakmap', WeakMap: 'bower-libs/WeakMap/weakmap',
rangy: 'bower-libs/rangy/rangy-core', 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: { shim: {
underscore: { underscore: {
@ -73,6 +74,9 @@ requirejs.config({
], ],
exports: 'jQuery.jGrowl' exports: 'jQuery.jGrowl'
}, },
diff_match_patch_uncompressed: {
exports: 'diff_match_patch'
},
rangy: { rangy: {
exports: 'rangy' exports: 'rangy'
}, },
@ -131,9 +135,6 @@ requirejs.config({
'jquery-waitforimages': [ 'jquery-waitforimages': [
'jquery' 'jquery'
], ],
'jquery-mousewheel': [
'jquery'
],
uilayout: [ uilayout: [
'jquery-ui-effect-slide' 'jquery-ui-effect-slide'
], ],

View File

@ -451,18 +451,6 @@ define([
}; };
function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) { 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) { if(takeServer) {
localDiscussion.selectionStart = realtimeDiscussion.get('selectionStart'); localDiscussion.selectionStart = realtimeDiscussion.get('selectionStart');
localDiscussion.selectionEnd = realtimeDiscussion.get('selectionEnd'); localDiscussion.selectionEnd = realtimeDiscussion.get('selectionEnd');

View File

@ -127,8 +127,8 @@
@popover-arrow-outer-color: @secondary-border-color; @popover-arrow-outer-color: @secondary-border-color;
@popover-title-bg: @transparent; @popover-title-bg: @transparent;
@alert-border-radius: 0; @alert-border-radius: 0;
@label-warning-bg: #da0; @label-warning-bg: darken(@logo-yellow, 4%);
@label-danger-bg: #d00; @label-danger-bg: darken(@logo-orange, 4%);
body { body {
@ -154,8 +154,8 @@ body {
.user-select(none); .user-select(none);
} }
.dropdown-menu, .modal-content, .panel-content, .search-bar .popover { .dropdown-menu, .modal-content, .panel-content, .search-bar, .popover {
.box-shadow(0 4px 12px rgba(0,0,0,.125)); .box-shadow(0 4px 16px rgba(0,0,0,.225));
} }
.dropdown-menu { .dropdown-menu {
@ -1066,15 +1066,15 @@ a {
} }
} }
&.replied { &.replied {
color: fade(@label-danger-bg, 35%); color: fade(@label-danger-bg, 45%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: fade(@label-danger-bg, 45%) !important; color: fade(@label-danger-bg, 80%) !important;
} }
} }
&.added { &.added {
color: fade(@label-warning-bg, 40%); color: fade(@label-warning-bg, 45%);
&:hover, &.active, &.active:hover { &:hover, &.active, &.active:hover {
color: fade(@label-warning-bg, 60%) !important; color: fade(@label-warning-bg, 80%) !important;
} }
} }
.transition(~"color ease-in-out .25s"); .transition(~"color ease-in-out .25s");