New undo manager

This commit is contained in:
benweet 2014-03-26 00:29:34 +00:00
parent 7f9747ef6c
commit 5361b8bee6
5 changed files with 222 additions and 147 deletions

View File

@ -386,32 +386,17 @@ define([
var pagedownEditor;
var $editorElt;
var fileDesc;
var documentContent;
core.initEditor = function(fileDescParam) {
if(fileDesc !== undefined) {
eventMgr.onFileClosed(fileDesc);
}
fileDesc = fileDescParam;
documentContent = undefined;
var initDocumentContent = fileDesc.content;
if(pagedownEditor !== undefined) {
// If the editor is already created
editor.contentElt.textContent = initDocumentContent;
pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
$editorElt.focus();
return;
return editor.undoManager.init();
}
var $previewContainerElt = $(".preview-container");
// Store preview scrollTop on scroll event
$previewContainerElt.scroll(function() {
if(documentContent !== undefined) {
fileDesc.previewScrollTop = $previewContainerElt.scrollTop();
}
});
// Create the converter and the editor
var converter = new Markdown.Converter();
var options = {
@ -425,15 +410,10 @@ define([
}
};
converter.setOptions(options);
pagedownEditor = new Markdown.Editor(converter, undefined, {
undoManager: editor.undoManager
});
if(window.lightMode) {
pagedownEditor = new Markdown.EditorLight(converter);
}
else {
pagedownEditor = new Markdown.Editor(converter, undefined, {
keyStrokes: shortcutMgr.getPagedownKeyStrokes()
});
}
// Custom insert link dialog
pagedownEditor.hooks.set("insertLinkDialog", function(callback) {
core.insertLinkCallback = callback;
@ -455,9 +435,7 @@ define([
eventMgr.onPagedownConfigure(pagedownEditor);
pagedownEditor.hooks.chain("onPreviewRefresh", eventMgr.onAsyncPreview);
pagedownEditor.run();
editor.contentElt.textContent = initDocumentContent;
pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
$editorElt.focus();
editor.undoManager.init();
// Hide default buttons
$(".wmd-button-row li").addClass("btn btn-success").css("left", 0).find("span").hide();

View File

@ -64,60 +64,138 @@ define([
});
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');
var currentMode;
editor.undoManager = (function() {
var undoManager = {
onButtonStateChange: function() {}
};
});
var undoStack = [];
var redoStack = [];
var lastTime;
var lastMode;
var currentState;
var selectionStartBefore;
var selectionEndBefore;
undoManager.setCommandMode = function() {
currentMode = 'command';
};
undoManager.setMode = function() {}; // For compatibility with PageDown
undoManager.saveState = function() {
if(currentMode == 'undoredo') {
currentMode = undefined;
return;
}
redoStack = [];
var currentTime = Date.now();
if((currentMode != lastMode && lastMode != 'newlines') || currentTime - lastTime > 1000) {
undoStack.push(currentState);
// Limit the size of the stack
if(undoStack.length === 100) {
undoStack.shift();
}
}
else {
selectionStartBefore = currentState.selectionStartBefore;
selectionEndBefore = currentState.selectionEndBefore;
}
currentState = {
selectionStartBefore: selectionStartBefore,
selectionEndBefore: selectionEndBefore,
selectionStartAfter: selectionStart,
selectionEndAfter: selectionEnd,
content: previousTextContent,
discussionList: JSON.stringify(fileDesc.discussionList)
};
lastTime = currentTime;
lastMode = currentMode;
currentMode = undefined;
undoManager.onButtonStateChange();
};
undoManager.saveSelectionState = _.debounce(function() {
if(currentMode === undefined) {
selectionStartBefore = selectionStart;
selectionEndBefore = selectionEnd;
}
}, 10);
undoManager.canUndo = function() {
return undoStack.length;
};
undoManager.canRedo = function() {
return redoStack.length;
};
function restoreState(state, selectionStart, selectionEnd) {
currentMode = 'undoredo';
inputElt.value = state.content;
fileDesc.discussionList = JSON.parse(state.discussionList);
selectionStartBefore = selectionStart;
selectionEndBefore = selectionEnd;
inputElt.setSelectionStartEnd(selectionStart, selectionEnd);
currentState = state;
lastMode = undefined;
undoManager.onButtonStateChange();
adjustCursorPosition();
}
undoManager.undo = function() {
var state = undoStack.pop();
if(!state) {
return;
}
redoStack.push(currentState);
restoreState(state, currentState.selectionStartBefore, currentState.selectionEndBefore);
};
undoManager.redo = function() {
var state = redoStack.pop();
if(!state) {
return;
}
undoStack.push(currentState);
restoreState(state, state.selectionStartAfter, state.selectionEndAfter);
};
undoManager.init = function() {
var content = fileDesc.content;
undoStack = [];
redoStack = [];
lastTime = 0;
currentState = {
selectionStartAfter: fileDesc.selectionStart,
selectionEndAfter: fileDesc.selectionEnd,
content: content,
discussionList: JSON.stringify(fileDesc.discussionList)
};
currentMode = undefined;
lastMode = undefined;
editor.contentElt.textContent = content;
};
return undoManager;
})();
function saveSelectionState() {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
var element = range.startContainer;
if ((inputElt.compareDocumentPosition(element) & 0x10)) {
var container = element;
var offset = range.startOffset;
do {
while (element = element.previousSibling) {
if (element.textContent) {
offset += element.textContent.length;
}
}
element = container = container.parentNode;
} while (element && element != inputElt);
selectionStart = offset;
selectionEnd = offset + (range + '').length;
}
}
if(fileChanged === false) {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
var element = range.startContainer;
if ((inputElt.compareDocumentPosition(element) & 0x10)) {
var container = element;
var offset = range.startOffset;
do {
while (element = element.previousSibling) {
if (element.textContent) {
offset += element.textContent.length;
}
}
element = container = container.parentNode;
} while (element && element != inputElt);
selectionStart = offset;
selectionEnd = offset + (range + '').length;
}
}
fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd;
}
editor.undoManager.saveSelectionState();
}
function checkContentChange() {
@ -130,65 +208,77 @@ define([
if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n';
}
var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent);
// Move comments according to changes
var updateDiscussionList = false;
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 >= endOffset) {
discussion.selectionEnd += diffOffset;
updateDiscussionList = true;
if(currentMode != 'undoredo') {
var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent);
// Move comments according to changes
var updateDiscussionList = false;
var startOffset = 0;
var discussionList = _.map(fileDesc.discussionList, _.identity);
fileDesc.newDiscussion && discussionList.push(fileDesc.newDiscussion);
changes.forEach(function(change) {
var changeType = change[0];
var changeText = change[1];
if(changeType === 0) {
startOffset += changeText.length;
return;
}
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;
var endOffset = startOffset;
var diffOffset = changeText.length;
if(changeType === -1) {
endOffset += diffOffset;
diffOffset = -diffOffset;
}
_.each(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;
});
startOffset = endOffset;
});
if(updateDiscussionList === true) {
fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage
if(updateDiscussionList === true) {
fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage
eventMgr.onCommentsChanged(fileDesc);
}
}
else {
// Comments have been restored by undo/redo
eventMgr.onCommentsChanged(fileDesc);
}
fileDesc.content = currentTextContent;
eventMgr.onContentChanged(fileDesc, currentTextContent);
currentMode = currentMode || 'typing';
previousTextContent = currentTextContent;
editor.undoManager.saveState();
}
else {
if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n';
fileDesc.content = currentTextContent;
}
eventMgr.onFileOpen(fileDesc, currentTextContent);
previewElt.scrollTop = fileDesc.previewScrollTop;
selectionStart = fileDesc.editorStart;
selectionEnd = fileDesc.editorEnd;
eventMgr.onFileOpen(fileDesc, currentTextContent);
previewElt.scrollTop = fileDesc.previewScrollTop;
scrollTop = fileDesc.editorScrollTop;
inputElt.scrollTop = scrollTop;
previousTextContent = currentTextContent;
fileChanged = false;
}
previousTextContent = currentTextContent;
}
function findOffset(ss) {
@ -203,11 +293,9 @@ define([
if (element) {
do {
var len = element.textContent.length;
if (offset <= ss && offset + len > ss) {
break;
}
offset += len;
} while (element = element.nextSibling);
}
@ -499,11 +587,11 @@ define([
}, 0);
})
.on('paste', function () {
pagedownEditor.undoManager.setMode("paste");
currentMode = 'paste';
adjustCursorPosition();
})
.on('cut', function () {
pagedownEditor.undoManager.setMode("cut");
currentMode = 'cut';
adjustCursorPosition();
});
@ -531,8 +619,6 @@ define([
indent: function (state, options) {
var lf = state.before.lastIndexOf('\n') + 1;
pagedownEditor.undoManager.setMode("typing");
if (options.inverse) {
if (/\s/.test(state.before.charAt(lf))) {
state.before = strSplice(state.before, lf, 1);
@ -582,7 +668,7 @@ define([
clearNewline = true;
}
pagedownEditor.undoManager.setMode("newlines");
currentMode = 'newlines';
state.before += '\n' + indent;
state.selection = '';

View File

@ -211,9 +211,7 @@ define([
addEventHook("onCursorCoordinates");
// Operations on comments
addEventHook("onDiscussionCreated");
addEventHook("onDiscussionRemoved");
addEventHook("onCommentAdded");
addEventHook("onCommentsChanged");
// Refresh twitter buttons
addEventHook("onTweet");

View File

@ -55,11 +55,35 @@ define([
};
var refreshId;
var cssApplier;
var currentFileDesc;
var currentContext;
function refreshDiscussions() {
if(currentFileDesc === undefined) {
return;
}
if(currentContext !== undefined) {
// Refresh conversation if popover is open
var context = currentContext;
if(context.discussion.discussionIndex) {
context.discussion = currentFileDesc.discussionList[context.discussion.discussionIndex];
context.popoverElt.querySelector('.discussion-comment-list').innerHTML = getDiscussionComments();
}
cssApplier.undoToRange(context.rangyRange);
context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd);
// Highlight selected text
context.rangyRange = rangy.createRange();
context.rangyRange.setStart(context.selectionRange.startContainer, context.selectionRange.startOffset);
context.rangyRange.setEnd(context.selectionRange.endContainer, context.selectionRange.endOffset);
setTimeout(function() { // Need to delay this because it's not refreshed properly
if(currentContext === context) {
cssApplier.applyToRange(context.rangyRange);
}
}, 50);
}
var author = storage['author.name'];
clearTimeout(refreshId);
commentEltList.forEach(function(commentElt) {
@ -105,7 +129,10 @@ define([
currentFileDesc === fileDesc && debouncedRefreshDiscussions();
};
var currentContext;
comments.onCommentsChanged = function(fileDesc) {
currentFileDesc === fileDesc && refreshDiscussions();
};
function closeCurrentPopover() {
currentContext && currentContext.$commentElt.popover('toggle').popover('destroy');
}
@ -114,18 +141,6 @@ define([
refreshDiscussions();
};
comments.onDiscussionCreated = function() {
refreshDiscussions();
};
comments.onDiscussionRemoved = function() {
refreshDiscussions();
};
comments.onCommentAdded = function() {
refreshDiscussions();
};
function getDiscussionComments() {
return currentContext.discussion.commentList.map(function(comment) {
return _.template(commentTmpl, {
@ -136,7 +151,7 @@ define([
}
comments.onReady = function() {
var cssApplier = rangy.createCssClassApplier("comment-highlight", {
cssApplier = rangy.createCssClassApplier("comment-highlight", {
normalize: false
});
var previousContent = '';
@ -213,6 +228,7 @@ define([
selectionEnd: selectionEnd,
commentList: []
};
currentFileDesc.newDiscussion = context.discussion;
}).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) {
// Move the popover in the margin
var context = currentContext;
@ -255,9 +271,7 @@ define([
closeCurrentPopover();
var discussionList = context.fileDesc.discussionList || {};
var isNew = false;
if(!context.discussion.discussionIndex) {
isNew = true;
// Create discussion index
var discussionIndex;
do {
@ -271,9 +285,7 @@ define([
content: content
});
context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
isNew ?
eventMgr.onDiscussionCreated(context.fileDesc, context.discussion) :
eventMgr.onCommentAdded(context.fileDesc, context.discussion);
eventMgr.onCommentsChanged(context.fileDesc);
inputElt.focus();
});
@ -297,7 +309,7 @@ define([
closeCurrentPopover();
delete context.fileDesc.discussionList[context.discussion.discussionIndex];
context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage
eventMgr.onDiscussionRemoved(context.fileDesc, context.discussion);
eventMgr.onCommentsChanged(context.fileDesc);
inputElt.focus();
});
}
@ -336,6 +348,7 @@ define([
// Remove highlight
cssApplier.undoToRange(currentContext.rangyRange);
currentContext = undefined;
delete currentFileDesc.newDiscussion;
});
};

View File

@ -514,13 +514,13 @@ define([
commentList: realtimeDiscussion.get('commentList').asArray()
};
localDiscussionList[discussionIndex] = discussion;
eventMgr.onDiscussionCreated(context.fileDesc, discussion);
eventMgr.onCommentsChanged(context.fileDesc);
}
});
context.fileDesc.discussionList = localDiscussionList; // Write in localStorage
}
eventMgr.addListener('onDiscussionCreated', function(fileDesc, discussion) {
eventMgr.addListener('onCommentsChanged', function(fileDesc, discussion) {
if(realtimeContext === undefined || realtimeContext.fileDesc !== fileDesc) {
return;
}