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",
"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"
}
}

View File

@ -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,34 +130,45 @@ 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;
}
var endOffset = startOffset;
var diffOffset = changeText.length;
if(changeType === -1) {
endOffset += diffOffset;
diffOffset = -diffOffset;
}
_.each(fileDesc.discussionList, function(discussion) {
// selectionEnd
if(discussion.selectionEnd >= change.endIndex) {
discussion.selectionEnd += endOffset;
if(discussion.selectionEnd >= endOffset) {
discussion.selectionEnd += diffOffset;
updateDiscussionList = true;
}
else if(discussion.selectionEnd > change.startIndex) {
discussion.selectionEnd = change.startIndex;
else if(discussion.selectionEnd > startOffset) {
discussion.selectionEnd = startOffset;
updateDiscussionList = true;
}
// selectionStart
if(discussion.selectionStart >= change.endIndex) {
discussion.selectionStart += endOffset;
if(discussion.selectionStart >= endOffset) {
discussion.selectionStart += diffOffset;
updateDiscussionList = true;
}
else if(discussion.selectionStart > change.startIndex) {
discussion.selectionStart = change.startIndex;
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;

View File

@ -18,6 +18,14 @@ define([
' <div class="comment-content"><%= content %></div>',
'</div>',
].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;
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);
var discussion = discussionList.pop();
var commentElt = crel('a', {
class: 'icon-comment'
});
@ -159,8 +164,9 @@ define([
if(titleLength > 20) {
title += '...';
}
title = title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
return '<a href="#" class="action-remove-discussion pull-right"><i class="icon-trash"></i></a>' + 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();
});
}

View File

@ -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'
],

View File

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

View File

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