diff --git a/img/icons.png b/img/icons.png index 20bae69a..6bada00b 100644 Binary files a/img/icons.png and b/img/icons.png differ diff --git a/js/async-runner.js b/js/async-runner.js index adc143b1..63a41253 100644 --- a/js/async-runner.js +++ b/js/async-runner.js @@ -81,7 +81,7 @@ define([ "core", "underscore" ], function(core) { } error = error || new Error("Unknown error"); if(error.message) { - core.showError(error.message); + core.showError(error); } runSafe(task, task.errorCallbacks, error); // Exit the current call stack diff --git a/js/core.js b/js/core.js index 5f95cdc3..6c63cc1a 100644 --- a/js/core.js +++ b/js/core.js @@ -1,7 +1,7 @@ define( - [ "jquery", "mathjax-editing", "bootstrap", "jgrowl", "layout", "Markdown.Editor", "storage", "config", + [ "jquery", "extension-manager", "bootstrap", "layout", "Markdown.Editor", "storage", "config", "underscore", "FileSaver", "css_browser_selector" ], - function($, mathjaxEditing) { + function($, extensionManager) { var core = {}; @@ -118,6 +118,7 @@ define( return url; }; + // Export data on disk core.saveFile = function(content, filename) { if(saveAs !== undefined) { var blob = new Blob([content], {type: "text/plain;charset=utf-8"}); @@ -141,26 +142,21 @@ define( } }; - // Used to show a notification message - core.showMessage = function(msg, iconClass, options) { - if(!msg) { - return; - } - var endOfMsg = msg.indexOf("|"); - if(endOfMsg !== -1) { - msg = msg.substring(0, endOfMsg); - if(!msg) { - return; - } - } - options = options || {}; - iconClass = iconClass || "icon-info-sign"; - $.jGrowl(" " + _.escape(msg), options); + // Log a message + core.showMessage = function(message) { + console.log(message); + extensionManager.onMessage(message); }; - // Used to show an error message - core.showError = function(msg) { - core.showMessage(msg, "icon-warning-sign"); + // Log an error + core.showError = function(error) { + console.error(error); + if(_.isString(error)) { + extensionManager.onMessage(error); + } + else if(_.isObject(error)) { + extensionManager.onMessage(error.message); + } }; // Offline management @@ -174,25 +170,19 @@ define( offlineTime = core.currentTime; if(core.isOffline === false) { core.isOffline = true; - core.showMessage("You are offline.", "icon-exclamation-sign msg-offline", { - sticky : true, - close : function() { - core.showMessage("You are back online!", "icon-signal"); - } + extensionManager.onOfflineChanged(true); + _.each(offlineListeners, function(listener) { + listener(); }); - for(var i=0; i offset) { - sectionText = text.substring(offset, matchOffset-1); - } - addMdSection(sectionText); - offset = matchOffset; - } - return ""; - } - ); - // Last section - // Consider wmd-input bottom padding and exclude \n\n previously added - padding += pxToFloat(editorElt.css('padding-bottom')); - addMdSection(text.substring(offset, text.length-2)); - - // Try to find corresponding sections in the preview - var previewElt = $("#wmd-preview"); - htmlSectionList = []; - var htmlSectionOffset = 0; - var previewScrollTop = previewElt.scrollTop(); - // Each title element is a section separator - previewElt.children("h1,h2,h3,h4,h5,h6").each(function() { - // Consider div scroll position and header element top margin - var newSectionOffset = $(this).position().top + previewScrollTop + pxToFloat($(this).css('margin-top')); - htmlSectionList.push({ - startOffset: htmlSectionOffset, - endOffset: newSectionOffset, - height: newSectionOffset - htmlSectionOffset - }); - htmlSectionOffset = newSectionOffset; - }); - // Last section - var scrollHeight = previewElt.prop('scrollHeight'); - htmlSectionList.push({ - startOffset: htmlSectionOffset, - endOffset: scrollHeight, - height: scrollHeight - htmlSectionOffset - }); - - // apply Scroll Link - lastEditorScrollTop = -9; - skipScrollLink = false; - scrollLink(); - }, 500); - - // -9 is less than -5 - var lastEditorScrollTop = -9; - var lastPreviewScrollTop = -9; - var skipScrollLink = false; - var scrollLink = _.debounce(function() { - if(skipScrollLink === true || mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) { - return; - } - var editorElt = $("#wmd-input"); - var editorScrollTop = editorElt.scrollTop(); - var previewElt = $("#wmd-preview"); - var previewScrollTop = previewElt.scrollTop(); - function animate(srcScrollTop, srcSectionList, destElt, destSectionList, lastDestScrollTop, callback) { - // Find the section corresponding to the offset - var sectionIndex = undefined; - var srcSection = _.find(srcSectionList, function(section, index) { - sectionIndex = index; - return srcScrollTop < section.endOffset; - }); - if(srcSection === undefined) { - // Something wrong in the algorithm... - return -9; - } - 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 - lastDestScrollTop) < 5) { - // Skip the animation in case it's not necessary - return; - } - destElt.animate({scrollTop: destScrollTop}, 600, function() { - callback(destScrollTop); - }); - } - if(Math.abs(editorScrollTop - lastEditorScrollTop) > 5) { - lastEditorScrollTop = editorScrollTop; - animate(editorScrollTop, mdSectionList, previewElt, htmlSectionList, lastPreviewScrollTop, function(destScrollTop) { - lastPreviewScrollTop = destScrollTop; - }); - } - else if(Math.abs(previewScrollTop - lastPreviewScrollTop) > 5) { - lastPreviewScrollTop = previewScrollTop; - animate(previewScrollTop, htmlSectionList, editorElt, mdSectionList, lastEditorScrollTop, function(destScrollTop) { - lastEditorScrollTop = destScrollTop; - }); - } - }, 600); - // Create the layout var layout = undefined; core.createLayout = function() { @@ -449,9 +305,7 @@ define( center__minWidth : 200, center__minHeight : 200 }; - if(core.settings.scrollLink === true) { - layoutGlobalConfig.onresize = buildSections; - } + extensionManager.onLayoutConfigure(layoutGlobalConfig); if (core.settings.layoutOrientation == "horizontal") { $(".ui-layout-south").remove(); $(".ui-layout-east").addClass("well").prop("id", "wmd-preview"); @@ -483,10 +337,7 @@ define( layout.allowOverflow('north'); }); - // ScrollLink - if(core.settings.scrollLink === true) { - $("#wmd-input, #wmd-preview").scroll(scrollLink); - } + extensionManager.onLayoutCreated(); }; core.layoutRefresh = function() { if(layout !== undefined) { @@ -498,57 +349,8 @@ define( // Create the PageDown editor var insertLinkCallback = undefined; core.createEditor = function(onTextChange) { - var firstChange = true; - skipScrollLink = true; - lastPreviewScrollTop = -9; - $("#wmd-input, #wmd-preview").scrollTop(0); - $("#wmd-button-bar").empty(); var converter = new Markdown.Converter(); - if(core.settings.converterType.indexOf("markdown-extra") === 0) { - // Markdown extra customized converter - var options = {}; - if(core.settings.converterType == "markdown-extra-prettify") { - options.highlighter = "prettify"; - } - Markdown.Extra.init(converter, options); - } - // Convert email addresses (not managed by pagedown) - converter.hooks.chain("postConversion", function(text) { - return text.replace(/<(mailto\:)?([^\s>]+@[^\s>]+\.\S+?)>/g, function(match, mailto, email) { - return '' + email + ''; - }); - }); var editor = new Markdown.Editor(converter); - // Prettify - if(core.settings.converterType == "markdown-extra-prettify") { - editor.hooks.chain("onPreviewRefresh", prettyPrint); - } - var previewRefreshFinished = function() { - if(viewerMode === false && core.settings.scrollLink === true) { - function updateScrollLinkSections() { - // Modify scroll position of the preview not the editor - lastEditorScrollTop = -9; - buildSections(); - // Preview may change if images are loading - $("#wmd-preview img").load(function() { - lastEditorScrollTop = -9; - buildSections(); - }); - } - // MathJax may have change the scroll position. Restore it - $("#wmd-preview").scrollTop(lastPreviewScrollTop); - _.defer(updateScrollLinkSections); - } - }; - // MathJax - if(core.settings.enableMathJax === true) { - mathjaxEditing.prepareWmdForMathJax(editor, [["$", "$"], ["\\\\(", "\\\\)"]], function() { - skipScrollLink = true; - }, previewRefreshFinished); - } - else { - editor.hooks.chain("onPreviewRefresh", previewRefreshFinished); - } // Custom insert link dialog editor.hooks.set("insertLinkDialog", function (callback) { insertLinkCallback = callback; @@ -570,6 +372,7 @@ define( return true; }); + var firstChange = true; var previewWrapper = function(makePreview) { return function() { if(firstChange !== true) { @@ -592,6 +395,18 @@ define( }; }; } + extensionManager.onEditorConfigure(editor); + editor.hooks.chain("onPreviewRefresh", extensionManager.onAsyncPreview); + + // Convert email addresses (not managed by pagedown) + converter.hooks.chain("postConversion", function(text) { + return text.replace(/<(mailto\:)?([^\s>]+@[^\s>]+\.\S+?)>/g, function(match, mailto, email) { + return '' + email + ''; + }); + }); + + $("#wmd-input, #wmd-preview").scrollTop(0); + $("#wmd-button-bar").empty(); editor.run(previewWrapper); firstChange = false; @@ -793,13 +608,12 @@ define( documentLoaded = true; runReadyCallbacks(); }); - + + core.onReady(function() { + extensionManager.init(core.settings.extensionConfig); + }); + core.onReady(extensionManager.onReady); core.onReady(function() { - // jGrowl configuration - $.jGrowl.defaults.life = 5000; - $.jGrowl.defaults.closer = false; - $.jGrowl.defaults.closeTemplate = ''; - $.jGrowl.defaults.position = 'bottom-right'; // Load theme list _.each(THEME_LIST, function(name, value) { diff --git a/js/extension-manager.js b/js/extension-manager.js new file mode 100644 index 00000000..ba9ccd82 --- /dev/null +++ b/js/extension-manager.js @@ -0,0 +1,77 @@ +define( [ + "underscore", + "extensions/notifications", + "extensions/markdown-extra", + "extensions/math-jax", + "extensions/scroll-link" +], function() { + + var extensionManager = {}; + + // Create a map with providerId: providerObject + var extensionList = _.chain(arguments) + .map(function(argument) { + return argument && argument.extensionId && argument; + }).compact().value(); + + // Return every named callbacks implemented in extensions + function getExtensionCallbackList(callbackName) { + return _.chain(extensionList) + .map(function(extension) { + return extension[callbackName]; + }).compact().value(); + } + + // Return a function that calls every callbacks from extensions + function createCallback(callbackName) { + var callbackList = getExtensionCallbackList(callbackName); + return function() { + var callbackArguments = arguments; + _.each(callbackList, function(callback) { + callback.apply(null, callbackArguments); + }); + }; + } + + // Add a callback to the extensionManager + function addCallback(callbackName) { + extensionManager[callbackName] = createCallback(callbackName); + } + + extensionManager.init = function(extensionConfigMap) { + // Set the extension configuration + extensionConfigMap = extensionConfigMap || {}; + _.each(extensionList, function(extension) { + extension.config = _.extend({}, extension.defaultConfig, extensionConfigMap[extension.extensionId]); + }); + }; + + addCallback("onReady"); + addCallback("onMessage"); + addCallback("onError"); + addCallback("onOfflineChanged"); + addCallback("onLayoutConfigure"); + addCallback("onLayoutCreated"); + addCallback("onEditorConfigure"); + + var onPreviewFinished = createCallback("onPreviewFinished"); + var onAsyncPreviewCallbackList = getExtensionCallbackList("onAsyncPreview"); + extensionManager["onAsyncPreview"] = function() { + // Call onPreviewFinished callbacks when all async preview are finished + var counter = 0; + function tryFinished() { + if(counter === onAsyncPreviewCallbackList.length) { + onPreviewFinished(); + } + } + _.each(onAsyncPreviewCallbackList, function(asyncPreviewCallback) { + asyncPreviewCallback(function() { + counter++; + tryFinished(); + }); + }); + tryFinished(); + }; + + return extensionManager; +}); \ No newline at end of file diff --git a/js/extensions/markdown-extra.js b/js/extensions/markdown-extra.js new file mode 100644 index 00000000..852ef0c6 --- /dev/null +++ b/js/extensions/markdown-extra.js @@ -0,0 +1,22 @@ +define( [ "Markdown.Extra" ], function() { + + var markdownExtra = { + extensionId: "markdownExtra", + extensionName: "Markdown Extra", + defaultConfig: { + prettify: true + } + }; + + markdownExtra.onEditorConfigure = function(editor) { + var converter = editor.getConverter(); + var options = {}; + if(markdownExtra.config.prettify === true) { + options.highlighter = "prettify"; + editor.hooks.chain("onPreviewRefresh", prettyPrint); + } + Markdown.Extra.init(converter, options); + }; + + return markdownExtra; +}); \ No newline at end of file diff --git a/js/extensions/math-jax.js b/js/extensions/math-jax.js new file mode 100644 index 00000000..dbf125fe --- /dev/null +++ b/js/extensions/math-jax.js @@ -0,0 +1,259 @@ +define( [ "MathJax" ], function($) { + + var mathJax = { + extensionId: "mathJax", + extensionName: "MathJax" + }; + + MathJax.Hub.Config({"HTML-CSS": {preferredFont: "TeX",availableFonts: ["STIX", "TeX"],linebreaks: {automatic: true},EqnChunk: (MathJax.Hub.Browser.isMobile ? 10 : 50), imageFont: null}, + tex2jax: {inlineMath: [["$", "$"], ["\\\\(", "\\\\)"]],displayMath: [["$$", "$$"], ["\\[", "\\]"]],processEscapes: true,ignoreClass: "tex2jax_ignore|dno"}, + TeX: {noUndefined: {attributes: {mathcolor: "red",mathbackground: "#FFEEEE",mathsize: "90%"}}, + Safe: {allow: {URLs: "safe",classes: "safe",cssIDs: "safe",styles: "safe",fontsize: "all"}}}, + messageStyle: "none" + }); + + var ready = false; // true after initial typeset is complete + var pending = false; // true when MathJax has been requested + var preview = null; // the preview container + var inline = "$"; // the inline math delimiter + + var blocks, start, end, last, braces; // used in searching for math + var math; // stores math until markdone is done + + var HUB = MathJax.Hub; + + // + // Runs after initial typeset + // + HUB.Queue(function() { + ready = true; + HUB.processUpdateTime = 50; // reduce update time so that we can cancel + // easier + HUB.Config({ "HTML-CSS" : { EqnChunk : 10, EqnChunkFactor : 1 }, // reduce + // chunk + // for + // more + // frequent + // updates + SVG : { EqnChunk : 10, EqnChunkFactor : 1 } }); + }); + + // + // The pattern for math delimiters and special symbols + // needed for searching for math in the page. + // + var SPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@)/i; + + // + // The math is in blocks i through j, so + // collect it into one block and clear the others. + // Replace &, <, and > by named entities. + // For IE, put
at the ends of comments since IE removes \n. + // Clear the current math positions and store the index of the + // math, then push the math string onto the storage array. + // + function processMath(i, j) { + var block = blocks.slice(i, j + 1).join("").replace(/&/g, "&") // use + // HTML + // entity + // for + // & + .replace(//g, ">") // use HTML entity for > + ; + if (HUB.Browser.isMSIE) { + block = block.replace(/(%[^\n]*)\n/g, "$1
\n") + } + while (j > i) { + blocks[j] = ""; + j-- + } + blocks[i] = "@@" + math.length + "@@"; + math.push(block); + start = end = last = null; + } + + // + // Break up the text into its component parts and search + // through them for math delimiters, braces, linebreaks, etc. + // Math delimiters must match and braces must balance. + // Don't allow math to pass through a double linebreak + // (which will be a paragraph). + // + function removeMath(text) { + start = end = last = null; // for tracking math delimiters + math = []; // stores math strings for latter + + blocks = text.replace(/\r\n?/g, "\n").split(SPLIT); + for ( var i = 1, m = blocks.length; i < m; i += 2) { + var block = blocks[i]; + if (block.charAt(0) === "@") { + // + // Things that look like our math markers will get + // stored and then retrieved along with the math. + // + blocks[i] = "@@" + math.length + "@@"; + math.push(block); + } else if (start) { + // + // If we are in math, look for the end delimiter, + // but don't go past double line breaks, and + // and balance braces within the math. + // + if (block === end) { + if (braces) { + last = i + } else { + processMath(start, i) + } + } else if (block.match(/\n.*\n/)) { + if (last) { + i = last; + processMath(start, i) + } + start = end = last = null; + braces = 0; + } else if (block === "{") { + braces++ + } else if (block === "}" && braces) { + braces-- + } + } else { + // + // Look for math start delimiters and when + // found, set up the end delimiter. + // + if (block === inline || block === "$$") { + start = i; + end = block; + braces = 0; + } else if (block.substr(1, 5) === "begin") { + start = i; + end = "\\end" + block.substr(6); + braces = 0; + } + } + } + if (last) { + processMath(start, last) + } + return blocks.join(""); + } + + // + // Put back the math strings that were saved, + // and clear the math array (no need to keep it around). + // + function replaceMath(text) { + text = text.replace(/@@(\d+)@@/g, function(match, n) { + return math[n] + }); + math = null; + return text; + } + + // + // This is run to restart MathJax after it has finished + // the previous run (that may have been canceled) + // + var afterRefreshCallback = undefined; + function RestartMJ() { + pending = false; + HUB.cancelTypeset = false; // won't need to do this in the future + HUB.Queue([ "Typeset", HUB, preview ]); + HUB.Queue(afterRefreshCallback); + } + + // + // When the preview changes, cancel MathJax and restart, + // if we haven't done that already. + // + function UpdateMJ() { + if (!pending && ready) { + pending = true; + HUB.Cancel(); + HUB.Queue(RestartMJ); + } + } + + // + // Save the preview ID and the inline math delimiter. + // Create a converter for the editor and register a preConversion hook + // to handle escaping the math. + // Create a preview refresh hook to handle starting MathJax. + // + mathJax.onEditorConfigure = function(editorObject) { + preview = document.getElementById("wmd-preview"); + + var converterObject = editorObject.getConverter(); + converterObject.hooks.chain("preConversion", removeMath); + converterObject.hooks.chain("postConversion", replaceMath); + editorObject.hooks.chain("onPreviewRefresh", UpdateMJ); + }; + mathJax.onAsyncPreview = function(callback) { + afterRefreshCallback = callback; + }; + + // + // Set up MathJax to allow canceling of typesetting, if it + // doesn't already have that. + // + if (!HUB.Cancel) { + + HUB.cancelTypeset = false; + var CANCELMESSAGE = "MathJax Canceled"; + + HUB.Register + .StartupHook( + "HTML-CSS Jax Config", + function() { + var HTMLCSS = MathJax.OutputJax["HTML-CSS"], TRANSLATE = HTMLCSS.Translate; + HTMLCSS.Augment({ Translate : function(script, state) { + if (HUB.cancelTypeset || state.cancelled) { + throw Error(CANCELMESSAGE) + } + return TRANSLATE.call(HTMLCSS, script, state); + } }); + }); + + HUB.Register.StartupHook("SVG Jax Config", function() { + var SVG = MathJax.OutputJax["SVG"], TRANSLATE = SVG.Translate; + SVG.Augment({ Translate : function(script, state) { + if (HUB.cancelTypeset || state.cancelled) { + throw Error(CANCELMESSAGE) + } + return TRANSLATE.call(SVG, script, state); + } }); + }); + + HUB.Register.StartupHook("TeX Jax Config", function() { + var TEX = MathJax.InputJax.TeX, TRANSLATE = TEX.Translate; + TEX.Augment({ Translate : function(script, state) { + if (HUB.cancelTypeset || state.cancelled) { + throw Error(CANCELMESSAGE) + } + return TRANSLATE.call(TEX, script, state); + } }); + }); + + var PROCESSERROR = HUB.processError; + HUB.processError = function(error, state, type) { + if (error.message !== CANCELMESSAGE) { + return PROCESSERROR.call(HUB, error, state, type) + } + MathJax.Message.Clear(0, 0); + state.jaxIDs = []; + state.jax = {}; + state.scripts = []; + state.i = state.j = 0; + state.cancelled = true; + return null; + }; + + HUB.Cancel = function() { + this.cancelTypeset = true + }; + } + + return mathJax; +}); \ No newline at end of file diff --git a/js/extensions/notifications.js b/js/extensions/notifications.js new file mode 100644 index 00000000..7b72e783 --- /dev/null +++ b/js/extensions/notifications.js @@ -0,0 +1,58 @@ +define( [ "jquery", "jgrowl", "underscore" ], function($) { + + var notifications = { + extensionId: "notifications", + extensionName: "Notifications", + defaultConfig: { + showingTime: 5000 + } + }; + + notifications.onReady = function() { + // jGrowl configuration + $.jGrowl.defaults.life = notifications.config.showingTime; + $.jGrowl.defaults.closer = false; + $.jGrowl.defaults.closeTemplate = ''; + $.jGrowl.defaults.position = 'bottom-right'; + }; + + function showMessage(msg, iconClass, options) { + if(!msg) { + return; + } + var endOfMsg = msg.indexOf("|"); + if(endOfMsg !== -1) { + msg = msg.substring(0, endOfMsg); + if(!msg) { + return; + } + } + options = options || {}; + iconClass = iconClass || "icon-info-sign"; + $.jGrowl(" " + _.escape(msg), options); + } + + notifications.onMessage = function(message) { + showMessage(message); + }; + + notifications.onError = function(error) { + showMessage(error, "icon-warning-sign"); + }; + + notifications.onOfflineChanged = function(isOffline) { + if(isOffline === true) { + showMessage("You are offline.", "icon-exclamation-sign msg-offline", { + sticky : true, + close : function() { + showMessage("You are back online!", "icon-signal"); + } + }); + } else { + $(".msg-offline").parents(".jGrowl-notification").trigger( + 'jGrowl.beforeClose'); + } + }; + + return notifications; +}); \ No newline at end of file diff --git a/js/extensions/scroll-link.js b/js/extensions/scroll-link.js new file mode 100644 index 00000000..f27faf4f --- /dev/null +++ b/js/extensions/scroll-link.js @@ -0,0 +1,187 @@ +define( [ "jquery", "underscore" ], function($) { + + var scrollLink = { + extensionId: "scrollLink", + extensionName: "Scroll Link" + }; + + var mdSectionList = []; + var htmlSectionList = []; + function pxToFloat(px) { + return parseFloat(px.substring(0, px.length-2)); + } + var buildSections = _.debounce(function() { + + // Try to find Markdown sections by looking for titles + var editorElt = $("#wmd-input"); + mdSectionList = []; + // This textarea is used to measure sections height + var textareaElt = $("#md-section-helper"); + // It has to be the same width than wmd-input + textareaElt.width(editorElt.width()); + // Consider wmd-input top padding + var padding = pxToFloat(editorElt.css('padding-top')); + var offset = 0, mdSectionOffset = 0; + function addMdSection(sectionText) { + var sectionHeight = padding; + if(sectionText !== undefined) { + textareaElt.val(sectionText); + sectionHeight += textareaElt.prop('scrollHeight'); + } + var newSectionOffset = mdSectionOffset + sectionHeight; + mdSectionList.push({ + startOffset: mdSectionOffset, + endOffset: newSectionOffset, + height: sectionHeight + }); + mdSectionOffset = newSectionOffset; + padding = 0; + } + // Create MD sections by finding title patterns (excluding gfm blocs) + var text = editorElt.val() + "\n\n"; + text.replace(/^```.*\n[\s\S]*?\n```|(^.+[ \t]*\n=+[ \t]*\n+|^.+[ \t]*\n-+[ \t]*\n+|^\#{1,6}[ \t]*.+?[ \t]*\#*\n+)/gm, + function(match, title, matchOffset) { + if(title) { + // We just found a title which means end of the previous section + // Exclude last \n of the section + var sectionText = undefined; + if(matchOffset > offset) { + sectionText = text.substring(offset, matchOffset-1); + } + addMdSection(sectionText); + offset = matchOffset; + } + return ""; + } + ); + // Last section + // Consider wmd-input bottom padding and exclude \n\n previously added + padding += pxToFloat(editorElt.css('padding-bottom')); + addMdSection(text.substring(offset, text.length-2)); + + // Try to find corresponding sections in the preview + var previewElt = $("#wmd-preview"); + htmlSectionList = []; + var htmlSectionOffset = 0; + var previewScrollTop = previewElt.scrollTop(); + // Each title element is a section separator + previewElt.children("h1,h2,h3,h4,h5,h6").each(function() { + // Consider div scroll position and header element top margin + var newSectionOffset = $(this).position().top + previewScrollTop + pxToFloat($(this).css('margin-top')); + htmlSectionList.push({ + startOffset: htmlSectionOffset, + endOffset: newSectionOffset, + height: newSectionOffset - htmlSectionOffset + }); + htmlSectionOffset = newSectionOffset; + }); + // Last section + var scrollHeight = previewElt.prop('scrollHeight'); + htmlSectionList.push({ + startOffset: htmlSectionOffset, + endOffset: scrollHeight, + height: scrollHeight - htmlSectionOffset + }); + + // apply Scroll Link + lastEditorScrollTop = -9; + skipScrollLink = false; + isScrollPreview = false; + runScrollLink(); + }, 500); + + // -9 is less than -5 + var lastEditorScrollTop = -9; + var lastPreviewScrollTop = -9; + var skipScrollLink = false; + var isScrollPreview = false; + var runScrollLink = _.debounce(function() { + if(skipScrollLink === true || mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) { + return; + } + var editorElt = $("#wmd-input"); + var editorScrollTop = editorElt.scrollTop(); + var previewElt = $("#wmd-preview"); + var previewScrollTop = previewElt.scrollTop(); + function animate(srcScrollTop, srcSectionList, destElt, destSectionList, lastDestScrollTop, callback) { + // Find the section corresponding to the offset + var sectionIndex = undefined; + var srcSection = _.find(srcSectionList, function(section, index) { + sectionIndex = index; + return srcScrollTop < section.endOffset; + }); + if(srcSection === undefined) { + // Something wrong in the algorithm... + return -9; + } + 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 - lastDestScrollTop) < 5) { + // Skip the animation in case it's not necessary + return; + } + destElt.animate({scrollTop: destScrollTop}, 600, function() { + callback(destScrollTop); + }); + } + // Perform the animation if diff > 5px + if(isScrollPreview === false && Math.abs(editorScrollTop - lastEditorScrollTop) > 5) { + // Animate the preview + lastEditorScrollTop = editorScrollTop; + animate(editorScrollTop, mdSectionList, previewElt, htmlSectionList, lastPreviewScrollTop, function(destScrollTop) { + lastPreviewScrollTop = destScrollTop; + }); + } + else if(Math.abs(previewScrollTop - lastPreviewScrollTop) > 5) { + // Animate the editor + lastPreviewScrollTop = previewScrollTop; + animate(previewScrollTop, htmlSectionList, editorElt, mdSectionList, lastEditorScrollTop, function(destScrollTop) { + lastEditorScrollTop = destScrollTop; + }); + } + }, 600); + + scrollLink.onLayoutConfigure = function(layoutConfig) { + layoutConfig.onresize = buildSections; + }; + + scrollLink.onLayoutCreated = function() { + $("#wmd-preview").scroll(function() { + isScrollPreview = true; + runScrollLink(); + }); + $("#wmd-input").scroll(function() { + isScrollPreview = false; + runScrollLink(); + }); + }; + + scrollLink.onEditorConfigure = function(editor) { + skipScrollLink = true; + lastPreviewScrollTop = 0; + editor.hooks.chain("onPreviewRefresh", function() { + skipScrollLink = true; + }); + }; + + scrollLink.onPreviewFinished = function() { + // MathJax may have change the scrolling position. Restore it. + if(lastPreviewScrollTop >= 0) { + $("#wmd-preview").scrollTop(lastPreviewScrollTop); + } + _.defer(function() { + // Modify scroll position of the preview not the editor + lastEditorScrollTop = -9; + buildSections(); + // Preview may change if images are loading + $("#wmd-preview img").load(function() { + lastEditorScrollTop = -9; + buildSections(); + }); + }); + }; + + return scrollLink; +}); \ No newline at end of file diff --git a/js/main.js b/js/main.js index 9607b599..5291ccf6 100644 --- a/js/main.js +++ b/js/main.js @@ -10,7 +10,7 @@ requirejs.config({ 'jgrowl': ['jquery'], 'layout': ['jquery-ui'], 'Markdown.Extra': ['Markdown.Converter', 'prettify'], - 'Markdown.Editor': ['Markdown.Extra'] + 'Markdown.Editor': ['Markdown.Converter'] } }); diff --git a/js/synchronizer.js b/js/synchronizer.js index 74d05f73..0584abb0 100644 --- a/js/synchronizer.js +++ b/js/synchronizer.js @@ -162,7 +162,6 @@ define(["jquery", "core", "dropbox-provider", "gdrive-provider", "underscore"], function isError(error) { if(error !== undefined) { - console.error(error); syncRunning = false; synchronizer.updateSyncButton(); return true;