2013-05-25 00:34:04 +00:00
|
|
|
define( [ "jquery", "underscore" ], function($) {
|
|
|
|
|
|
|
|
var scrollLink = {
|
|
|
|
extensionId: "scrollLink",
|
2013-05-25 18:13:59 +00:00
|
|
|
extensionName: "Scroll Link",
|
|
|
|
optional: true,
|
|
|
|
settingsBloc: [
|
|
|
|
'<p>Binds together editor and preview scrollbars.</p>',
|
|
|
|
'<blockquote class="muted"><b>NOTE:</b> ',
|
|
|
|
'The mapping between Markdown and HTML is based on the position of the title elements (h1, h2, ...) in the page. ',
|
2013-05-26 01:10:58 +00:00
|
|
|
'Therefore, if your document does not contain any title, the mapping will be linear and consequently less accurate.',
|
2013-05-25 18:13:59 +00:00
|
|
|
'</bloquote>'
|
|
|
|
].join("")
|
2013-05-25 00:34:04 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
|
|
|
});
|