Extension pattern
This commit is contained in:
parent
a271abc2bc
commit
8879d327dd
BIN
img/icons.png
BIN
img/icons.png
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB |
@ -81,7 +81,7 @@ define([ "core", "underscore" ], function(core) {
|
|||||||
}
|
}
|
||||||
error = error || new Error("Unknown error");
|
error = error || new Error("Unknown error");
|
||||||
if(error.message) {
|
if(error.message) {
|
||||||
core.showError(error.message);
|
core.showError(error);
|
||||||
}
|
}
|
||||||
runSafe(task, task.errorCallbacks, error);
|
runSafe(task, task.errorCallbacks, error);
|
||||||
// Exit the current call stack
|
// Exit the current call stack
|
||||||
|
272
js/core.js
272
js/core.js
@ -1,7 +1,7 @@
|
|||||||
define(
|
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" ],
|
"underscore", "FileSaver", "css_browser_selector" ],
|
||||||
function($, mathjaxEditing) {
|
function($, extensionManager) {
|
||||||
|
|
||||||
var core = {};
|
var core = {};
|
||||||
|
|
||||||
@ -118,6 +118,7 @@ define(
|
|||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export data on disk
|
||||||
core.saveFile = function(content, filename) {
|
core.saveFile = function(content, filename) {
|
||||||
if(saveAs !== undefined) {
|
if(saveAs !== undefined) {
|
||||||
var blob = new Blob([content], {type: "text/plain;charset=utf-8"});
|
var blob = new Blob([content], {type: "text/plain;charset=utf-8"});
|
||||||
@ -141,26 +142,21 @@ define(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Used to show a notification message
|
// Log a message
|
||||||
core.showMessage = function(msg, iconClass, options) {
|
core.showMessage = function(message) {
|
||||||
if(!msg) {
|
console.log(message);
|
||||||
return;
|
extensionManager.onMessage(message);
|
||||||
}
|
|
||||||
var endOfMsg = msg.indexOf("|");
|
|
||||||
if(endOfMsg !== -1) {
|
|
||||||
msg = msg.substring(0, endOfMsg);
|
|
||||||
if(!msg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options = options || {};
|
|
||||||
iconClass = iconClass || "icon-info-sign";
|
|
||||||
$.jGrowl("<i class='icon-white " + iconClass + "'></i> " + _.escape(msg), options);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Used to show an error message
|
// Log an error
|
||||||
core.showError = function(msg) {
|
core.showError = function(error) {
|
||||||
core.showMessage(msg, "icon-warning-sign");
|
console.error(error);
|
||||||
|
if(_.isString(error)) {
|
||||||
|
extensionManager.onMessage(error);
|
||||||
|
}
|
||||||
|
else if(_.isObject(error)) {
|
||||||
|
extensionManager.onMessage(error.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Offline management
|
// Offline management
|
||||||
@ -174,25 +170,19 @@ define(
|
|||||||
offlineTime = core.currentTime;
|
offlineTime = core.currentTime;
|
||||||
if(core.isOffline === false) {
|
if(core.isOffline === false) {
|
||||||
core.isOffline = true;
|
core.isOffline = true;
|
||||||
core.showMessage("You are offline.", "icon-exclamation-sign msg-offline", {
|
extensionManager.onOfflineChanged(true);
|
||||||
sticky : true,
|
_.each(offlineListeners, function(listener) {
|
||||||
close : function() {
|
listener();
|
||||||
core.showMessage("You are back online!", "icon-signal");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
for(var i=0; i<offlineListeners.length; i++) {
|
|
||||||
offlineListeners[i]();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
core.setOnline = function() {
|
core.setOnline = function() {
|
||||||
if(core.isOffline === true) {
|
if(core.isOffline === true) {
|
||||||
$(".msg-offline").parents(".jGrowl-notification").trigger(
|
|
||||||
'jGrowl.beforeClose');
|
|
||||||
core.isOffline = false;
|
core.isOffline = false;
|
||||||
for(var i=0; i<offlineListeners.length; i++) {
|
extensionManager.onOfflineChanged(false);
|
||||||
offlineListeners[i]();
|
_.each(offlineListeners, function(listener) {
|
||||||
}
|
listener();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function checkOnline() {
|
function checkOnline() {
|
||||||
@ -295,140 +285,6 @@ define(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Used by Scroll Link feature
|
|
||||||
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;
|
|
||||||
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
|
// Create the layout
|
||||||
var layout = undefined;
|
var layout = undefined;
|
||||||
core.createLayout = function() {
|
core.createLayout = function() {
|
||||||
@ -449,9 +305,7 @@ define(
|
|||||||
center__minWidth : 200,
|
center__minWidth : 200,
|
||||||
center__minHeight : 200
|
center__minHeight : 200
|
||||||
};
|
};
|
||||||
if(core.settings.scrollLink === true) {
|
extensionManager.onLayoutConfigure(layoutGlobalConfig);
|
||||||
layoutGlobalConfig.onresize = buildSections;
|
|
||||||
}
|
|
||||||
if (core.settings.layoutOrientation == "horizontal") {
|
if (core.settings.layoutOrientation == "horizontal") {
|
||||||
$(".ui-layout-south").remove();
|
$(".ui-layout-south").remove();
|
||||||
$(".ui-layout-east").addClass("well").prop("id", "wmd-preview");
|
$(".ui-layout-east").addClass("well").prop("id", "wmd-preview");
|
||||||
@ -483,10 +337,7 @@ define(
|
|||||||
layout.allowOverflow('north');
|
layout.allowOverflow('north');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ScrollLink
|
extensionManager.onLayoutCreated();
|
||||||
if(core.settings.scrollLink === true) {
|
|
||||||
$("#wmd-input, #wmd-preview").scroll(scrollLink);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
core.layoutRefresh = function() {
|
core.layoutRefresh = function() {
|
||||||
if(layout !== undefined) {
|
if(layout !== undefined) {
|
||||||
@ -498,57 +349,8 @@ define(
|
|||||||
// Create the PageDown editor
|
// Create the PageDown editor
|
||||||
var insertLinkCallback = undefined;
|
var insertLinkCallback = undefined;
|
||||||
core.createEditor = function(onTextChange) {
|
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();
|
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 '<a href="mailto:' + email + '">' + email + '</a>';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
var editor = new Markdown.Editor(converter);
|
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
|
// Custom insert link dialog
|
||||||
editor.hooks.set("insertLinkDialog", function (callback) {
|
editor.hooks.set("insertLinkDialog", function (callback) {
|
||||||
insertLinkCallback = callback;
|
insertLinkCallback = callback;
|
||||||
@ -570,6 +372,7 @@ define(
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var firstChange = true;
|
||||||
var previewWrapper = function(makePreview) {
|
var previewWrapper = function(makePreview) {
|
||||||
return function() {
|
return function() {
|
||||||
if(firstChange !== true) {
|
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 '<a href="mailto:' + email + '">' + email + '</a>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#wmd-input, #wmd-preview").scrollTop(0);
|
||||||
|
$("#wmd-button-bar").empty();
|
||||||
editor.run(previewWrapper);
|
editor.run(previewWrapper);
|
||||||
firstChange = false;
|
firstChange = false;
|
||||||
|
|
||||||
@ -793,13 +608,12 @@ define(
|
|||||||
documentLoaded = true;
|
documentLoaded = true;
|
||||||
runReadyCallbacks();
|
runReadyCallbacks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
core.onReady(function() {
|
||||||
|
extensionManager.init(core.settings.extensionConfig);
|
||||||
|
});
|
||||||
|
core.onReady(extensionManager.onReady);
|
||||||
core.onReady(function() {
|
core.onReady(function() {
|
||||||
// jGrowl configuration
|
|
||||||
$.jGrowl.defaults.life = 5000;
|
|
||||||
$.jGrowl.defaults.closer = false;
|
|
||||||
$.jGrowl.defaults.closeTemplate = '';
|
|
||||||
$.jGrowl.defaults.position = 'bottom-right';
|
|
||||||
|
|
||||||
// Load theme list
|
// Load theme list
|
||||||
_.each(THEME_LIST, function(name, value) {
|
_.each(THEME_LIST, function(name, value) {
|
||||||
|
77
js/extension-manager.js
Normal file
77
js/extension-manager.js
Normal file
@ -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;
|
||||||
|
});
|
22
js/extensions/markdown-extra.js
Normal file
22
js/extensions/markdown-extra.js
Normal file
@ -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;
|
||||||
|
});
|
259
js/extensions/math-jax.js
Normal file
259
js/extensions/math-jax.js
Normal file
@ -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 <br> 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 <
|
||||||
|
.replace(/>/g, ">") // use HTML entity for >
|
||||||
|
;
|
||||||
|
if (HUB.Browser.isMSIE) {
|
||||||
|
block = block.replace(/(%[^\n]*)\n/g, "$1<br/>\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;
|
||||||
|
});
|
58
js/extensions/notifications.js
Normal file
58
js/extensions/notifications.js
Normal file
@ -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("<i class='icon-white " + iconClass + "'></i> " + _.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;
|
||||||
|
});
|
187
js/extensions/scroll-link.js
Normal file
187
js/extensions/scroll-link.js
Normal file
@ -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;
|
||||||
|
});
|
@ -10,7 +10,7 @@ requirejs.config({
|
|||||||
'jgrowl': ['jquery'],
|
'jgrowl': ['jquery'],
|
||||||
'layout': ['jquery-ui'],
|
'layout': ['jquery-ui'],
|
||||||
'Markdown.Extra': ['Markdown.Converter', 'prettify'],
|
'Markdown.Extra': ['Markdown.Converter', 'prettify'],
|
||||||
'Markdown.Editor': ['Markdown.Extra']
|
'Markdown.Editor': ['Markdown.Converter']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -162,7 +162,6 @@ define(["jquery", "core", "dropbox-provider", "gdrive-provider", "underscore"],
|
|||||||
|
|
||||||
function isError(error) {
|
function isError(error) {
|
||||||
if(error !== undefined) {
|
if(error !== undefined) {
|
||||||
console.error(error);
|
|
||||||
syncRunning = false;
|
syncRunning = false;
|
||||||
synchronizer.updateSyncButton();
|
synchronizer.updateSyncButton();
|
||||||
return true;
|
return true;
|
||||||
|
Loading…
Reference in New Issue
Block a user