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

View File

@ -64,35 +64,113 @@ define([
}); });
var previousTextContent; var previousTextContent;
var undoManager = {}; var currentMode;
(function() { editor.undoManager = (function() {
var undoStack = []; // A stack of undo states var undoManager = {
var stackPtr = 0; // The index of the current state onButtonStateChange: function() {}
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 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() { function saveSelectionState() {
if(fileChanged === false) {
var selection = window.getSelection(); var selection = window.getSelection();
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0); var range = selection.getRangeAt(0);
@ -114,10 +192,10 @@ define([
selectionEnd = offset + (range + '').length; selectionEnd = offset + (range + '').length;
} }
} }
if(fileChanged === false) {
fileDesc.editorStart = selectionStart; fileDesc.editorStart = selectionStart;
fileDesc.editorEnd = selectionEnd; fileDesc.editorEnd = selectionEnd;
} }
editor.undoManager.saveSelectionState();
} }
function checkContentChange() { function checkContentChange() {
@ -130,10 +208,13 @@ define([
if(!/\n$/.test(currentTextContent)) { if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n'; currentTextContent += '\n';
} }
if(currentMode != 'undoredo') {
var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent); var changes = diffMatchPatch.diff_main(previousTextContent, currentTextContent);
// Move comments according to changes // Move comments according to changes
var updateDiscussionList = false; var updateDiscussionList = false;
var startOffset = 0; var startOffset = 0;
var discussionList = _.map(fileDesc.discussionList, _.identity);
fileDesc.newDiscussion && discussionList.push(fileDesc.newDiscussion);
changes.forEach(function(change) { changes.forEach(function(change) {
var changeType = change[0]; var changeType = change[0];
var changeText = change[1]; var changeText = change[1];
@ -147,7 +228,7 @@ define([
endOffset += diffOffset; endOffset += diffOffset;
diffOffset = -diffOffset; diffOffset = -diffOffset;
} }
_.each(fileDesc.discussionList, function(discussion) { _.each(discussionList, function(discussion) {
// selectionEnd // selectionEnd
if(discussion.selectionEnd >= endOffset) { if(discussion.selectionEnd >= endOffset) {
discussion.selectionEnd += diffOffset; discussion.selectionEnd += diffOffset;
@ -171,24 +252,33 @@ define([
}); });
if(updateDiscussionList === true) { if(updateDiscussionList === true) {
fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage 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; fileDesc.content = currentTextContent;
eventMgr.onContentChanged(fileDesc, currentTextContent); eventMgr.onContentChanged(fileDesc, currentTextContent);
currentMode = currentMode || 'typing';
previousTextContent = currentTextContent;
editor.undoManager.saveState();
} }
else { else {
if(!/\n$/.test(currentTextContent)) { if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n'; currentTextContent += '\n';
fileDesc.content = currentTextContent; fileDesc.content = currentTextContent;
} }
eventMgr.onFileOpen(fileDesc, currentTextContent);
previewElt.scrollTop = fileDesc.previewScrollTop;
selectionStart = fileDesc.editorStart; selectionStart = fileDesc.editorStart;
selectionEnd = fileDesc.editorEnd; selectionEnd = fileDesc.editorEnd;
eventMgr.onFileOpen(fileDesc, currentTextContent);
previewElt.scrollTop = fileDesc.previewScrollTop;
scrollTop = fileDesc.editorScrollTop; scrollTop = fileDesc.editorScrollTop;
inputElt.scrollTop = scrollTop; inputElt.scrollTop = scrollTop;
previousTextContent = currentTextContent;
fileChanged = false; fileChanged = false;
} }
previousTextContent = currentTextContent;
} }
function findOffset(ss) { function findOffset(ss) {
@ -203,11 +293,9 @@ define([
if (element) { if (element) {
do { do {
var len = element.textContent.length; var len = element.textContent.length;
if (offset <= ss && offset + len > ss) { if (offset <= ss && offset + len > ss) {
break; break;
} }
offset += len; offset += len;
} while (element = element.nextSibling); } while (element = element.nextSibling);
} }
@ -499,11 +587,11 @@ define([
}, 0); }, 0);
}) })
.on('paste', function () { .on('paste', function () {
pagedownEditor.undoManager.setMode("paste"); currentMode = 'paste';
adjustCursorPosition(); adjustCursorPosition();
}) })
.on('cut', function () { .on('cut', function () {
pagedownEditor.undoManager.setMode("cut"); currentMode = 'cut';
adjustCursorPosition(); adjustCursorPosition();
}); });
@ -531,8 +619,6 @@ define([
indent: function (state, options) { indent: function (state, options) {
var lf = state.before.lastIndexOf('\n') + 1; var lf = state.before.lastIndexOf('\n') + 1;
pagedownEditor.undoManager.setMode("typing");
if (options.inverse) { if (options.inverse) {
if (/\s/.test(state.before.charAt(lf))) { if (/\s/.test(state.before.charAt(lf))) {
state.before = strSplice(state.before, lf, 1); state.before = strSplice(state.before, lf, 1);
@ -582,7 +668,7 @@ define([
clearNewline = true; clearNewline = true;
} }
pagedownEditor.undoManager.setMode("newlines"); currentMode = 'newlines';
state.before += '\n' + indent; state.before += '\n' + indent;
state.selection = ''; state.selection = '';

View File

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

View File

@ -55,11 +55,35 @@ define([
}; };
var refreshId; var refreshId;
var cssApplier;
var currentFileDesc; var currentFileDesc;
var currentContext;
function refreshDiscussions() { function refreshDiscussions() {
if(currentFileDesc === undefined) { if(currentFileDesc === undefined) {
return; 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']; var author = storage['author.name'];
clearTimeout(refreshId); clearTimeout(refreshId);
commentEltList.forEach(function(commentElt) { commentEltList.forEach(function(commentElt) {
@ -105,7 +129,10 @@ define([
currentFileDesc === fileDesc && debouncedRefreshDiscussions(); currentFileDesc === fileDesc && debouncedRefreshDiscussions();
}; };
var currentContext; comments.onCommentsChanged = function(fileDesc) {
currentFileDesc === fileDesc && refreshDiscussions();
};
function closeCurrentPopover() { function closeCurrentPopover() {
currentContext && currentContext.$commentElt.popover('toggle').popover('destroy'); currentContext && currentContext.$commentElt.popover('toggle').popover('destroy');
} }
@ -114,18 +141,6 @@ define([
refreshDiscussions(); refreshDiscussions();
}; };
comments.onDiscussionCreated = function() {
refreshDiscussions();
};
comments.onDiscussionRemoved = function() {
refreshDiscussions();
};
comments.onCommentAdded = function() {
refreshDiscussions();
};
function getDiscussionComments() { function getDiscussionComments() {
return currentContext.discussion.commentList.map(function(comment) { return currentContext.discussion.commentList.map(function(comment) {
return _.template(commentTmpl, { return _.template(commentTmpl, {
@ -136,7 +151,7 @@ define([
} }
comments.onReady = function() { comments.onReady = function() {
var cssApplier = rangy.createCssClassApplier("comment-highlight", { cssApplier = rangy.createCssClassApplier("comment-highlight", {
normalize: false normalize: false
}); });
var previousContent = ''; var previousContent = '';
@ -213,6 +228,7 @@ define([
selectionEnd: selectionEnd, selectionEnd: selectionEnd,
commentList: [] commentList: []
}; };
currentFileDesc.newDiscussion = context.discussion;
}).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) {
// Move the popover in the margin // Move the popover in the margin
var context = currentContext; var context = currentContext;
@ -255,9 +271,7 @@ define([
closeCurrentPopover(); closeCurrentPopover();
var discussionList = context.fileDesc.discussionList || {}; var discussionList = context.fileDesc.discussionList || {};
var isNew = false;
if(!context.discussion.discussionIndex) { if(!context.discussion.discussionIndex) {
isNew = true;
// Create discussion index // Create discussion index
var discussionIndex; var discussionIndex;
do { do {
@ -271,9 +285,7 @@ define([
content: content content: content
}); });
context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage
isNew ? eventMgr.onCommentsChanged(context.fileDesc);
eventMgr.onDiscussionCreated(context.fileDesc, context.discussion) :
eventMgr.onCommentAdded(context.fileDesc, context.discussion);
inputElt.focus(); inputElt.focus();
}); });
@ -297,7 +309,7 @@ define([
closeCurrentPopover(); closeCurrentPopover();
delete context.fileDesc.discussionList[context.discussion.discussionIndex]; delete context.fileDesc.discussionList[context.discussion.discussionIndex];
context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage
eventMgr.onDiscussionRemoved(context.fileDesc, context.discussion); eventMgr.onCommentsChanged(context.fileDesc);
inputElt.focus(); inputElt.focus();
}); });
} }
@ -336,6 +348,7 @@ define([
// Remove highlight // Remove highlight
cssApplier.undoToRange(currentContext.rangyRange); cssApplier.undoToRange(currentContext.rangyRange);
currentContext = undefined; currentContext = undefined;
delete currentFileDesc.newDiscussion;
}); });
}; };

View File

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