Extension pattern

This commit is contained in:
benweet 2013-05-25 01:34:04 +01:00
parent a271abc2bc
commit 8879d327dd
10 changed files with 648 additions and 232 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -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

View File

@ -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("<i class='icon-white " + iconClass + "'></i> " + _.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<offlineListeners.length; i++) {
offlineListeners[i]();
}
}
};
core.setOnline = function() {
if(core.isOffline === true) {
$(".msg-offline").parents(".jGrowl-notification").trigger(
'jGrowl.beforeClose');
core.isOffline = false;
for(var i=0; i<offlineListeners.length; i++) {
offlineListeners[i]();
}
extensionManager.onOfflineChanged(false);
_.each(offlineListeners, function(listener) {
listener();
});
}
};
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
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 '<a href="mailto:' + email + '">' + email + '</a>';
});
});
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 '<a href="mailto:' + email + '">' + email + '</a>';
});
});
$("#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) {

77
js/extension-manager.js Normal file
View 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;
});

View 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
View 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, "&amp;") // use
// HTML
// entity
// for
// &
.replace(/</g, "&lt;") // use HTML entity for <
.replace(/>/g, "&gt;") // 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;
});

View 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;
});

View 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;
});

View File

@ -10,7 +10,7 @@ requirejs.config({
'jgrowl': ['jquery'],
'layout': ['jquery-ui'],
'Markdown.Extra': ['Markdown.Converter', 'prettify'],
'Markdown.Editor': ['Markdown.Extra']
'Markdown.Editor': ['Markdown.Converter']
}
});

View File

@ -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;