diff --git a/public/res/core.js b/public/res/core.js index 749b1e0c..976fe204 100644 --- a/public/res/core.js +++ b/public/res/core.js @@ -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(); diff --git a/public/res/editor.js b/public/res/editor.js index 3a7acb42..dbf42268 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -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 = ''; diff --git a/public/res/eventMgr.js b/public/res/eventMgr.js index c7c80d7c..b79ab2a2 100644 --- a/public/res/eventMgr.js +++ b/public/res/eventMgr.js @@ -211,9 +211,7 @@ define([ addEventHook("onCursorCoordinates"); // Operations on comments - addEventHook("onDiscussionCreated"); - addEventHook("onDiscussionRemoved"); - addEventHook("onCommentAdded"); + addEventHook("onCommentsChanged"); // Refresh twitter buttons addEventHook("onTweet"); diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index 8121f7ff..30ad075c 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -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; }); }; diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js index 7388c3b0..cb63a245 100644 --- a/public/res/providers/gdriveProviderBuilder.js +++ b/public/res/providers/gdriveProviderBuilder.js @@ -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; }