From 582337d5955f84a451448421bf3e086e91e50740 Mon Sep 17 00:00:00 2001 From: benweet Date: Tue, 10 Sep 2013 00:32:24 +0100 Subject: [PATCH] Switch to ACE editor --- res/classes/FileDescriptor.js | 2 +- res/core.js | 157 ++++++++++++-------------- res/eventMgr.js | 5 +- res/extensions/dialogOpenHarddrive.js | 2 +- res/extensions/documentSelector.js | 10 +- res/extensions/emailConverter.js | 2 +- res/extensions/markdownExtra.js | 2 +- res/extensions/mathJax.js | 2 +- res/extensions/partialRendering.js | 2 +- res/extensions/scrollLink.js | 139 +++++++++++------------ res/extensions/toc.js | 2 +- res/fileMgr.js | 15 ++- res/html/bodyIndex.html | 3 +- res/html/bodyViewer.html | 2 +- res/libs/Markdown.Editor.js | 50 ++++---- res/libs/acemode_highlight_rules.js | 6 +- res/providers/gdriveProvider.js | 2 +- res/styles/main.less | 10 +- 18 files changed, 198 insertions(+), 215 deletions(-) diff --git a/res/classes/FileDescriptor.js b/res/classes/FileDescriptor.js index 6ce95aea..90e3f893 100644 --- a/res/classes/FileDescriptor.js +++ b/res/classes/FileDescriptor.js @@ -18,7 +18,7 @@ define([ return new Range(rangeComponents[0], rangeComponents[1], rangeComponents[2], rangeComponents[3]); } catch(e) { - return new Range(); + return new Range(0, 0, 0, 0); } })(); this._editorEnd = parseInt(localStorage[fileIndex + ".editorEnd"]) || 0; diff --git a/res/core.js b/res/core.js index bc3360c8..851b6544 100644 --- a/res/core.js +++ b/res/core.js @@ -187,56 +187,64 @@ define([ } } - // Create the layout + // Create ACE editor var aceEditor = undefined; - function createLayout() { + function createAceEditor() { aceEditor = ace.edit("wmd-input"); aceEditor.renderer.setShowGutter(false); aceEditor.renderer.setShowPrintMargin(false); aceEditor.renderer.setPrintMarginColumn(false); - aceEditor.renderer.setPadding(12); + aceEditor.renderer.setPadding(10); aceEditor.session.setUseWrapMode(true); aceEditor.session.setMode("libs/acemode"); // Make bold titles... - (function(bgTokenizer) { - var worker = bgTokenizer.$worker; - bgTokenizer.$worker = function() { - bgTokenizer.currentLine = bgTokenizer.currentLine ? bgTokenizer.currentLine - 1 : 0; - worker(); - _.each(bgTokenizer.lines, function(line, i) { - if(i !== 0 && line && line.length !== 0 && line[0].type.indexOf("markup.heading.multi") === 0) { - _.each(bgTokenizer.lines[i-1], function(previousLineObject) { - previousLineObject.type = "markup.heading.prev.multi"; - }); + (function(self) { + function customWorker() { + if (!self.running) { return; } + + var workerStart = new Date(); + var startLine = self.currentLine; + var doc = self.doc; + + var processedLines = 0; + + var len = doc.getLength(); + while (self.currentLine < len) { + self.$tokenizeRow(self.currentLine); + while (self.lines[self.currentLine]) { + var line = self.lines[self.currentLine]; + if(line.length !== 0 && line[0].type.indexOf("markup.heading.multi") === 0) { + _.each(self.lines[self.currentLine-1], function(previousLineObject) { + previousLineObject.type = "markup.heading.prev.multi"; + }); + } + self.currentLine++; } - }); + + // only check every 5 lines + processedLines ++; + if ((processedLines % 5 == 0) && (new Date() - workerStart) > 20) { + self.fireUpdateEvent(startLine, self.currentLine-1); + self.running = setTimeout(customWorker, 20); + return; + } + } + + self.running = false; + + self.fireUpdateEvent(startLine, len - 1); + } + self.$worker = function() { + self.currentLine = self.currentLine ? self.currentLine - 1 : 0; + customWorker(); }; })(aceEditor.session.bgTokenizer); - - - window.wmdInput = { - editor: aceEditor, - focus: function() { - aceEditor.focus(); - } - }; - Object.defineProperty(window.wmdInput, 'value', { - get: function() { - return aceEditor.getValue(); - }, - set: function(value) { - aceEditor.setValue(value); - } - }); - Object.defineProperty(window.wmdInput, 'scrollTop', { - get: function() { - return aceEditor.renderer.getScrollTop(); - }, - set: function(value) { - aceEditor.renderer.scrollToY(value); - } - }); - + eventMgr.onAceCreated(aceEditor); + window.aceEditor = aceEditor; + } + + // Create the layout + function createLayout() { var layoutGlobalConfig = { closable: true, resizable: false, @@ -319,7 +327,7 @@ define([ var editor = undefined; var fileDesc = undefined; var documentContent = undefined; - var $editorElt = undefined; + var UndoManager = require("ace/undomanager").UndoManager; core.initEditor = function(fileDescParam) { if(fileDesc !== undefined) { eventMgr.onFileClosed(fileDesc); @@ -329,15 +337,12 @@ define([ var initDocumentContent = fileDesc.content; aceEditor.setValue(initDocumentContent, -1); - _.defer(function() { - aceEditor.session.getUndoManager().reset(); - }); + aceEditor.getSession().setUndoManager(new UndoManager()); + if(editor !== undefined) { // If the editor is already created aceEditor.selection.setSelectionRange(fileDesc.editorSelectRange); - aceEditor.renderer.scrollToY(fileDesc.editorScrollTop); aceEditor.focus(); - eventMgr.onFileOpen(fileDesc); editor.refreshPreview(); return; } @@ -345,9 +350,12 @@ define([ var $previewContainerElt = $(".preview-container"); // Store editor scrollTop on scroll event + var debouncedUpdateScroll = _.debounce(function() { + fileDesc.editorScrollTop = aceEditor.renderer.getScrollTop(); + }, 100); aceEditor.session.on('changeScrollTop', function() { if(documentContent !== undefined) { - fileDesc.editorScrollTop = aceEditor.renderer.getScrollTop(); + debouncedUpdateScroll(); } }); // Store editor selection on change @@ -419,9 +427,12 @@ define([ var debouncedMakePreview = _.debounce(makePreview, 500); return function() { if(documentContent === undefined) { + aceEditor.renderer.scrollToY(fileDesc.editorScrollTop); makePreview(); - //$editorElt.scrollTop(fileDesc.editorScrollTop); $previewContainerElt.scrollTop(fileDesc.previewScrollTop); + _.defer(function() { + eventMgr.onFileOpen(fileDesc); + }); } else { debouncedMakePreview(); @@ -441,13 +452,12 @@ define([ }; }; } - eventMgr.onEditorConfigure(editor); + eventMgr.onPagedownConfigure(editor); editor.hooks.chain("onPreviewRefresh", eventMgr.onAsyncPreview); - editor.run(previewWrapper); + editor.run(aceEditor, previewWrapper); // editor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, // fileDesc.editorEnd, fileDesc.editorScrollTop); aceEditor.selection.setSelectionRange(fileDesc.editorSelectRange); - aceEditor.renderer.scrollToY(fileDesc.editorScrollTop); aceEditor.focus(); // Hide default buttons @@ -470,23 +480,6 @@ define([ var $btnGroupElt = $('.wmd-button-group4'); $("#wmd-undo-button").append($('')).appendTo($btnGroupElt); $("#wmd-redo-button").append($('')).appendTo($btnGroupElt); - - eventMgr.onFileOpen(fileDesc); - }; - - // Used to lock the editor from the user interaction during asynchronous - // tasks - var uiLocked = false; - core.lockUI = function(param) { - uiLocked = param; - $editorElt.prop("disabled", uiLocked); - $(".navbar-inner .btn").toggleClass("blocked", uiLocked); - if(uiLocked) { - $(".lock-ui").removeClass("hide"); - } - else { - $(".lock-ui").addClass("hide"); - } }; // Initialize multiple things and then fire eventMgr.onReady @@ -569,29 +562,18 @@ define([ } }); - // UI layout - createLayout(); - $editorElt = $("#wmd-input"); - // Editor's textarea - $("#wmd-input, #md-section-helper").css({ + $("#wmd-input").css({ // Apply editor font "font-family": settings.editorFontFamily, "font-size": settings.editorFontSize + "px", - "line-height": Math.round(settings.editorFontSize * (20 / 14)) + "px" + "line-height": Math.round(settings.editorFontSize * (20 / 12)) + "px" }); - // Handle tab key - $editorElt.keydown(function(e) { - if(e.keyCode === 9) { - var value = $editorElt.val(); - var start = this.selectionStart; - var end = this.selectionEnd; - $(this).val(value.substring(0, start) + "\t" + value.substring(end)); - this.selectionStart = this.selectionEnd = start + 1; - e.preventDefault(); - } - }); + // ACE editor + createAceEditor(); + // UI layout + createLayout(); // Do periodic tasks intervalId = window.setInterval(function() { @@ -617,13 +599,14 @@ define([ isModalShown = true; }).on('shown.bs.modal', function() { // Focus on the first input when modal opens - _.defer(function(elt) { + var elt = $(this); + setTimeout(function() { elt.find("input:enabled:visible:first").focus(); - }, $(this)); + }, 50); }).on('hidden.bs.modal', function() { // Focus on the editor when modal is gone isModalShown = false; - $editorElt.focus(); + aceEditor.focus(); // Revert to current theme when settings modal is closed applyTheme(localStorage.theme); }).keyup(function(e) { diff --git a/res/eventMgr.js b/res/eventMgr.js index d5a3d4b8..08a7d330 100644 --- a/res/eventMgr.js +++ b/res/eventMgr.js @@ -171,8 +171,11 @@ define([ addEventHook("onLayoutResize"); // Operations on PageDown - addEventHook("onEditorConfigure"); + addEventHook("onPagedownConfigure"); addEventHook("onSectionsCreated"); + + // Operation on ACE + addEventHook("onAceCreated"); var onPreviewFinished = createEventHook("onPreviewFinished"); var onAsyncPreviewListenerList = getExtensionListenerList("onAsyncPreview"); diff --git a/res/extensions/dialogOpenHarddrive.js b/res/extensions/dialogOpenHarddrive.js index db1b91d7..1b01f4fd 100644 --- a/res/extensions/dialogOpenHarddrive.js +++ b/res/extensions/dialogOpenHarddrive.js @@ -30,7 +30,7 @@ define([ var files = (evt.dataTransfer || evt.target).files; $(".modal-import-harddrive-markdown, .modal-import-harddrive-html").modal("hide"); _.each(files, function(file) { - if($(evt.target).is("#wmd-input") && file.name.match(/.(jpe?g|png|gif)$/)) { + if($(evt.target).is("#wmd-input *") && file.name.match(/.(jpe?g|png|gif)$/)) { return; } var reader = new FileReader(); diff --git a/res/extensions/documentSelector.js b/res/extensions/documentSelector.js index 70544e06..839a37c9 100644 --- a/res/extensions/documentSelector.js +++ b/res/extensions/documentSelector.js @@ -29,6 +29,11 @@ define([ newConfig.shortcutNext = utils.getInputTextValue("#input-document-selector-shortcut-next", event); }; + var aceEditor = undefined; + documentSelector.onAceCreated = function(aceEditorParam) { + aceEditor = aceEditorParam; + }; + var fileMgr = undefined; documentSelector.onFileMgrCreated = function(fileMgrParameter) { fileMgr = fileMgrParameter; @@ -41,7 +46,6 @@ define([ ' ', '' ].join(''); - var $editorElt = undefined; var dropdownElt = undefined; var liEltMap = undefined; var liEltList = undefined; @@ -70,7 +74,7 @@ define([ fileMgr.selectFile(fileDesc); } else { - $editorElt.focus(); + aceEditor.focus(); } }); }); @@ -91,8 +95,6 @@ define([ documentSelector.onPublishRemoved = buildSelector; documentSelector.onReady = function() { - $editorElt = $("#wmd-input"); - if(documentSelector.config.orderBy == "title") { sortFunction = function(fileDesc) { return fileDesc.title.toLowerCase(); diff --git a/res/extensions/emailConverter.js b/res/extensions/emailConverter.js index 7d85a704..3220ebbf 100644 --- a/res/extensions/emailConverter.js +++ b/res/extensions/emailConverter.js @@ -5,7 +5,7 @@ define([ var emailConverter = new Extension("emailConverter", "Markdown Email", true); emailConverter.settingsBlock = '

Converts email adresses in the form <email@example.com> into clickable links.

'; - emailConverter.onEditorConfigure = function(editor) { + emailConverter.onPagedownConfigure = function(editor) { editor.getConverter().hooks.chain("postConversion", function(text) { return text.replace(/<(mailto\:)?([^\s>]+@[^\s>]+\.\S+?)>/g, function(match, mailto, email) { return '' + email + ''; diff --git a/res/extensions/markdownExtra.js b/res/extensions/markdownExtra.js index 1c8b79ed..f5ffc3ea 100644 --- a/res/extensions/markdownExtra.js +++ b/res/extensions/markdownExtra.js @@ -44,7 +44,7 @@ define([ newConfig.highlighter = utils.getInputValue("#input-markdownextra-highlighter"); }; - markdownExtra.onEditorConfigure = function(editor) { + markdownExtra.onPagedownConfigure = function(editor) { var converter = editor.getConverter(); var options = { extensions: markdownExtra.config.extensions diff --git a/res/extensions/mathJax.js b/res/extensions/mathJax.js index 8b395ab2..e023541d 100644 --- a/res/extensions/mathJax.js +++ b/res/extensions/mathJax.js @@ -61,7 +61,7 @@ define([ }); }; - mathJax.onEditorConfigure = function(editorObject) { + mathJax.onPagedownConfigure = function(editorObject) { t = document.getElementById("preview-contents"); var converter = editorObject.getConverter(); diff --git a/res/extensions/partialRendering.js b/res/extensions/partialRendering.js index 94e14cb8..d63cc6dc 100644 --- a/res/extensions/partialRendering.js +++ b/res/extensions/partialRendering.js @@ -172,7 +172,7 @@ define([ } } - partialRendering.onEditorConfigure = function(editor) { + partialRendering.onPagedownConfigure = function(editor) { converter = editor.getConverter(); converter.hooks.chain("preConversion", function(text) { var result = _.map(modifiedSections, function(section) { diff --git a/res/extensions/scrollLink.js b/res/extensions/scrollLink.js index 2b66fa8b..a32ae9fb 100644 --- a/res/extensions/scrollLink.js +++ b/res/extensions/scrollLink.js @@ -10,14 +10,17 @@ define([ var scrollLink = new Extension("scrollLink", "Scroll Link", true, true); scrollLink.settingsBlock = scrollLinkSettingsBlockHTML; + var aceEditor = undefined; + scrollLink.onAceCreated = function(aceEditorParam) { + aceEditor = aceEditorParam; + }; + var sectionList = undefined; scrollLink.onSectionsCreated = function(sectionListParam) { sectionList = sectionListParam; }; - var $editorElt = undefined; var $previewElt = undefined; - var $textareaElt = undefined; var mdSectionList = []; var htmlSectionList = []; function pxToFloat(px) { @@ -27,57 +30,21 @@ define([ var lastPreviewScrollTop = undefined; var buildSections = _.debounce(function() { - // Try to find Markdown sections by looking for titles mdSectionList = []; - // It has to be the same width as wmd-input - $textareaElt.width($editorElt.width()); - // Consider wmd-input top padding (will be used for 1st and last - // section) - var padding = pxToFloat($editorElt.css('padding-top')); + var mdTextOffset = 0; var mdSectionOffset = 0; - function addMdSection(sectionText) { - var sectionHeight = padding; - if(sectionText !== undefined) { - $textareaElt.val(sectionText); - sectionHeight += $textareaElt.prop('scrollHeight'); - } - var newSectionOffset = mdSectionOffset + sectionHeight; + _.each(sectionList, function(sectionText) { + mdTextOffset += sectionText.length; + var documentPosition = aceEditor.session.doc.indexToPosition(mdTextOffset); + var screenPosition = aceEditor.session.documentToScreenPosition(documentPosition.row, documentPosition.column); + var newSectionOffset = screenPosition.row * aceEditor.renderer.lineHeight; + var sectionHeight = newSectionOffset - mdSectionOffset; mdSectionList.push({ startOffset: mdSectionOffset, endOffset: newSectionOffset, height: sectionHeight }); mdSectionOffset = newSectionOffset; - padding = 0; - } - _.each(sectionList, function(sectionText, index) { - if(index !== sectionList.length - 1) { - if(sectionText.length === 0) { - sectionText = undefined; - } - else { - // Remove the last \n preceding the next title - sectionText = sectionText.substring(0, sectionText.length - 1); - } - } - else { - // Last section - // Consider wmd-input bottom padding and keep last empty line - padding += pxToFloat($editorElt.css('padding-bottom')); - } - addMdSection(sectionText); - }); - - // Apply a coef to manage divergence in some browsers - var theoricalHeight = _.last(mdSectionList).endOffset; - var realHeight = $editorElt[0].scrollHeight; - var coef = realHeight/theoricalHeight; - mdSectionList = _.map(mdSectionList, function(mdSection) { - return { - startOffset: mdSection.startOffset * coef, - endOffset: mdSection.endOffset * coef, - height: mdSection.height * coef, - }; }); // Try to find corresponding sections in the preview @@ -114,11 +81,13 @@ define([ var isScrollPreview = false; var doScrollLink = _.debounce(function() { if(mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) { + // Delay + doScrollLink(); return; } - var editorScrollTop = $editorElt.scrollTop(); + var editorScrollTop = aceEditor.renderer.getScrollTop(); var previewScrollTop = $previewElt.scrollTop(); - function animate(srcScrollTop, srcSectionList, destElt, destSectionList, currentDestScrollTop, callback) { + function getDestScrollTop(srcScrollTop, srcSectionList, destSectionList) { // Find the section corresponding to the offset var sectionIndex = undefined; var srcSection = _.find(srcSectionList, function(section, index) { @@ -131,38 +100,56 @@ define([ } var posInSection = (srcScrollTop - srcSection.startOffset) / srcSection.height; var destSection = destSectionList[sectionIndex]; - var destScrollTop = destSection.startOffset + destSection.height * posInSection; - destScrollTop = _.min([ - destScrollTop, - destElt.prop('scrollHeight') - destElt.outerHeight() - ]); - if(Math.abs(destScrollTop - currentDestScrollTop) <= 9) { - // Skip the animation if diff is <= 9 - callback(currentDestScrollTop); - return; - } - destElt.animate({ - scrollTop: destScrollTop - }, 500, function() { - callback(destScrollTop); - }); + return destSection.startOffset + destSection.height * posInSection; } // Perform the animation if diff > 9px if(isScrollEditor === true && Math.abs(editorScrollTop - lastEditorScrollTop) > 9) { isScrollEditor = false; // Animate the preview lastEditorScrollTop = editorScrollTop; - animate(editorScrollTop, mdSectionList, $previewElt, htmlSectionList, previewScrollTop, function(destScrollTop) { - lastPreviewScrollTop = destScrollTop; - }); + var destScrollTop = getDestScrollTop(editorScrollTop, mdSectionList, htmlSectionList); + destScrollTop = _.min([ + destScrollTop, + $previewElt.prop('scrollHeight') - $previewElt.outerHeight() + ]); + if(Math.abs(destScrollTop - previewScrollTop) <= 9) { + // Skip the animation if diff is <= 9 + lastPreviewScrollTop = previewScrollTop; + } + else { + $previewElt.animate({ + scrollTop: destScrollTop + }, 'easeOutQuad', function() { + lastPreviewScrollTop = destScrollTop; + }); + } } else if(isScrollPreview === true && Math.abs(previewScrollTop - lastPreviewScrollTop) > 9) { isScrollPreview = false; // Animate the editor lastPreviewScrollTop = previewScrollTop; - animate(previewScrollTop, htmlSectionList, $editorElt, mdSectionList, editorScrollTop, function(destScrollTop) { - lastEditorScrollTop = destScrollTop; - }); + var destScrollTop = getDestScrollTop(previewScrollTop, htmlSectionList, mdSectionList); + destScrollTop = _.min([ + destScrollTop, + aceEditor.session.getScreenLength() * aceEditor.renderer.lineHeight - aceEditor.renderer.$size.scrollerHeight + ]); + if(Math.abs(destScrollTop - editorScrollTop) <= 9) { + // Skip the animation if diff is <= 9 + lastEditorScrollTop = editorScrollTop; + } + else { + $("
").animate({ + value: destScrollTop - editorScrollTop + }, { + easing: 'easeOutQuad', + step: function(now) { + aceEditor.session.setScrollTop(editorScrollTop + now); + }, + complete: function() { + lastEditorScrollTop = destScrollTop; + } + }); + } } }, 500); @@ -171,13 +158,13 @@ define([ buildSections(); }; + scrollLink.onFileClosed = function() { + mdSectionList = []; + }; + scrollLink.onReady = function() { - $editorElt = $("#wmd-input"); $previewElt = $(".preview-container"); - - // This textarea is used to measure sections height - $textareaElt = $("#md-section-helper"); - + $previewElt.bind("keyup mouseup mousewheel", function() { isScrollPreview = true; isScrollEditor = false; @@ -188,15 +175,15 @@ define([ isScrollEditor = false; doScrollLink(); }); - $editorElt.bind("keyup mouseup mousewheel", function() { + aceEditor.session.on("changeScrollTop", function(e) { isScrollEditor = true; isScrollPreview = false; doScrollLink(); }); }; - + var $previewContentsElt = undefined; - scrollLink.onEditorConfigure = function(editor) { + scrollLink.onPagedownConfigure = function(editor) { $previewContentsElt = $("#preview-contents"); editor.getConverter().hooks.chain("postConversion", function(text) { // To avoid losing scrolling position before elements are fully diff --git a/res/extensions/toc.js b/res/extensions/toc.js index 2dd810ea..d6cda052 100644 --- a/res/extensions/toc.js +++ b/res/extensions/toc.js @@ -114,7 +114,7 @@ define([ return '
\n
    \n' + elementList.join("") + '
\n
\n'; } - toc.onEditorConfigure = function(editor) { + toc.onPagedownConfigure = function(editor) { previewContentsElt = document.getElementById('preview-contents'); var tocEltList = document.querySelectorAll('.table-of-contents'); var tocExp = new RegExp("^" + toc.config.marker + "$", "g"); diff --git a/res/fileMgr.js b/res/fileMgr.js index 9f804a71..8105dfbd 100644 --- a/res/fileMgr.js +++ b/res/fileMgr.js @@ -150,9 +150,12 @@ define([ }); }; - eventMgr.addListener("onReady", function() { - var $editorElt = $("#wmd-input"); + var aceEditor = undefined; + eventMgr.addListener('onAceCreated', function(aceEditorParam) { + aceEditor = aceEditorParam; + }); + eventMgr.addListener("onReady", function() { fileMgr.selectFile(); var $fileTitleElt = $('.file-title-navbar'); @@ -160,10 +163,6 @@ define([ $(".action-create-file").click(function() { var fileDesc = fileMgr.createFile(); fileMgr.selectFile(fileDesc); - var wmdInput = $editorElt.focus().get(0); - if(wmdInput.setSelectionRange) { - wmdInput.setSelectionRange(0, 0); - } $fileTitleElt.click(); }); $(".action-remove-file").click(function() { @@ -189,7 +188,7 @@ define([ eventMgr.onTitleChanged(fileDesc); } $fileTitleInputElt.val(fileDesc.title); - $editorElt.focus(); + aceEditor.focus(); } $fileTitleInputElt.blur(function() { applyTitle(); @@ -206,7 +205,7 @@ define([ window.location.href = "."; }); $(".action-edit-document").click(function() { - var content = $editorElt.val(); + var content = aceEditor.getValue(); var title = fileMgr.currentFile.title; var fileDesc = fileMgr.createFile(title, content); fileMgr.selectFile(fileDesc); diff --git a/res/html/bodyIndex.html b/res/html/bodyIndex.html index 75368cde..20c9a189 100644 --- a/res/html/bodyIndex.html +++ b/res/html/bodyIndex.html @@ -39,7 +39,7 @@
-
+
@@ -1076,6 +1076,5 @@ -
\ No newline at end of file diff --git a/res/html/bodyViewer.html b/res/html/bodyViewer.html index df4918db..152ef5b7 100644 --- a/res/html/bodyViewer.html +++ b/res/html/bodyViewer.html @@ -34,7 +34,7 @@
- +