New undo manager
This commit is contained in:
parent
7f9747ef6c
commit
5361b8bee6
@ -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();
|
||||
|
@ -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 = '';
|
||||
|
@ -211,9 +211,7 @@ define([
|
||||
addEventHook("onCursorCoordinates");
|
||||
|
||||
// Operations on comments
|
||||
addEventHook("onDiscussionCreated");
|
||||
addEventHook("onDiscussionRemoved");
|
||||
addEventHook("onCommentAdded");
|
||||
addEventHook("onCommentsChanged");
|
||||
|
||||
// Refresh twitter buttons
|
||||
addEventHook("onTweet");
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user