Added diff-match-patch
This commit is contained in:
parent
8b565e32ce
commit
7f9747ef6c
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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, '&').replace(/</g, '<').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();
|
||||
});
|
||||
}
|
||||
|
@ -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'
|
||||
],
|
||||
|
@ -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');
|
||||
|
@ -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");
|
||||
|
Loading…
Reference in New Issue
Block a user