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;