Scroll Link feature

This commit is contained in:
benweet 2013-04-27 00:08:13 +01:00
parent ff6ee6be4e
commit b16d6cc7d1
9 changed files with 427 additions and 53 deletions

View File

@ -1 +1 @@
CACHE MANIFEST # v11 CACHE: index.html css/main-min.css js/main-min.js js/require.js img/ajax-loader.gif img/glyphicons-halflings.png img/glyphicons-halflings-white.png img/icons.png img/stackedit-32.ico img/stackedit-promo.png NETWORK: *
CACHE MANIFEST # v12 CACHE: index.html css/main-min.css js/main-min.js js/require.js img/ajax-loader.gif img/glyphicons-halflings.png img/glyphicons-halflings-white.png img/icons.png img/stackedit-32.ico img/stackedit-promo.png NETWORK: *

18
css/main-min.css vendored
View File

@ -5473,7 +5473,7 @@ hr {
#wmd-input,#wmd-preview {
position: absolute;
}
#wmd-input {
#wmd-input, #md-section-helper {
font-family: "Courier New", Courier, monospace;
resize: none;
border: none !important;
@ -5656,4 +5656,20 @@ blockquote p {
margin-bottom: 20px;
font-size: 14px;
line-height: 20px;
}
#md-section-helper {
position: absolute;
top: -100px;
height: 1px;
padding: 0 6px;
overflow-y: scroll;
z-index: -1;
}
.gecko #md-section-helper {
height: 40px;
}
.opera #md-section-helper {
top: 0;
}

View File

@ -162,7 +162,7 @@ hr {
position: absolute;
}
#wmd-input {
#wmd-input, #md-section-helper {
font-family: "Courier New", Courier, monospace;
resize: none;
border: none !important;
@ -384,4 +384,23 @@ blockquote p {
margin-bottom: 20px;
font-size: 14px;
line-height: 20px;
}
#md-section-helper {
position: absolute;
top: -100px;
height: 1px;
padding: 0 6px;
overflow-y: scroll;
z-index: -1;
}
.gecko #md-section-helper {
/* Firefox doesn't show the scrollbar if height is less than 40px */
height: 40px;
}
.opera #md-section-helper {
/* Opera needs to have the textarea in the viewport to evaluate size correctly */
top: 0;
}

View File

@ -11,13 +11,15 @@
content="StackEdit is a free, open-source Markdown editor based on PageDown, the Markdown library used by Stack Overflow and the other Stack Exchange sites.">
<meta name="author" content="Benoit Schweblin">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="css/main-min.css" rel="stylesheet" media="screen">
<script>
// http://.../?debug to serve original JavaScript files for debug
// http://.../?debug to serve original CSS/JavaScript files for debug
var dep = "main-min";
var css = "css/main-min.css";
if (location.search.indexOf("debug") !== -1) {
dep = "main";
css = "css/main.css";
}
document.write('<link href="' + css + '" rel="stylesheet" media="screen">');
var require = { baseUrl : "js", deps : [ dep ] };
</script>
<script src="js/require.js"></script>
@ -432,6 +434,13 @@
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input-settings-scroll-link">Scroll Link <a
href="#" class="tooltip-scroll-link">(?)</a></label>
<div class="controls">
<input type="checkbox" id="input-settings-scroll-link" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="input-settings-converter-type">Converter</label>
<div class="controls">
@ -528,7 +537,10 @@
<dl>
<dt>Credit:</dt>
<dd>
<a target="_blank" href="http://twitter.github.com/bootstrap/">Bootstrap</a>
<a target="_blank" href="http://twitter.github.io/bootstrap/">Bootstrap</a>
</dd>
<dd>
<a target="_blank" href="https://github.com/rafaelp/css_browser_selector/">CSS Browser Selector</a>
</dd>
<dd>
<a target="_blank" href="https://github.com/dropbox/dropbox-js">Dropbox-js</a>
@ -594,7 +606,8 @@
class="btn btn-primary">Reload</a>
</div>
</div>
<textarea id="md-section-helper"></textarea>
<div id="dropboxjs" data-app-key="x0k2l8puemfvg0o"></div>
</body>
</html>

View File

@ -17,15 +17,15 @@
// patch for ie7
if (!Array.indexOf) {
Array.prototype.indexOf = function(obj){
for(var i = 0; i < this.length; i++){
if(this[i] == obj){
return i;
}
}
return -1;
}
}
Array.prototype.indexOf = function(obj) {
for (var i = 0; i < this.length; i++) {
if (this[i] == obj) {
return i;
}
}
return -1;
}
}
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
@ -262,10 +262,10 @@
// TODO: use sentinels. Should we just add/remove them in doConversion?
// TODO: better matches for id / class attributes
var attrBlock = "\\{\\s*[.|#][^}]+\\}";
var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})\\s+(" + attrBlock + ")\\s*(\\n|0x03)", "gm");
var hdrAttributesB = new RegExp("^(.*)\\s+(" + attrBlock + ")\\s*\\n" +
var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})\\s+(" + attrBlock + ")[ \\t]*(\\n|0x03)", "gm");
var hdrAttributesB = new RegExp("^(.*)\\s+(" + attrBlock + ")[ \\t]*\\n" +
"(?=[\\-|=]+\\s*(\\n|0x03))", "gm"); // underline lookahead
var fcbAttributes = new RegExp("^(```[^{]*)\\s+(" + attrBlock + ")\\s*\\n" +
var fcbAttributes = new RegExp("^(```[^{]*)\\s+(" + attrBlock + ")[ \\t]*\\n" +
"(?=([\\s\\S]*?)\\n```\\s*(\\n|0x03))", "gm");
var self = this;
@ -281,7 +281,7 @@
Markdown.Extra.prototype.applyAttributeBlocks = function(text) {
var self = this;
var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\s]*' +
'(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*</\\2>))', "gm");
'(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?</\\2>))', "gm");
text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) {
if (!tag) // no following header or fenced code block.
return '';

View File

@ -1,6 +1,6 @@
define(
[ "jquery", "bootstrap", "jgrowl", "layout", "Markdown.Editor", "config",
"underscore", "FileSaver" ],
"underscore", "FileSaver", "css_browser_selector" ],
function($) {
var core = {};
@ -208,6 +208,7 @@ define(
core.settings = {
converterType : "markdown-extra-prettify",
layoutOrientation : "horizontal",
scrollLink : true,
editorFontSize : 14,
commitMsg : "Published by StackEdit",
template : ['<!DOCTYPE html>\n',
@ -228,6 +229,9 @@ define(
$("input:radio[name=radio-layout-orientation][value="
+ core.settings.layoutOrientation + "]").prop("checked", true);
// Scroll Link
$("#input-settings-scroll-link").prop("checked", core.settings.scrollLink);
// Converter type
$("#input-settings-converter-type").val(core.settings.converterType);
@ -251,6 +255,9 @@ define(
// Converter type
newSettings.converterType = $("#input-settings-converter-type").val();
// Scroll Link
newSettings.scrollLink = $("#input-settings-scroll-link").prop("checked");
// Editor font size
newSettings.editorFontSize = core.getInputIntValue($("#input-settings-editor-font-size"), event, 1, 99);
@ -266,6 +273,131 @@ 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) {
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
addMdSection(text.substring(offset, matchOffset-1));
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
});
/*
console.log("mdSectionList: " + _.map(mdSectionList, function(section) {
return section.endOffset;
}));
*/
// apply Scroll Link
lastEditorScrollTop = -99;
lastPreviewScrollTop = -99;
scrollLink();
}, 800);
var lastEditorScrollTop = -99;
var lastPreviewScrollTop = -99;
var scrollLink = _.debounce(function() {
if(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) {
// 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 0;
}
var posInSection = (srcScrollTop - srcSection.startOffset) / srcSection.height;
var destSection = destSectionList[sectionIndex];
var destScrollTop = destSection.startOffset + destSection.height * posInSection;
destElt.animate({scrollTop: destScrollTop}, 800, function() {
lastEditorScrollTop = editorElt.scrollTop();
lastPreviewScrollTop = previewElt.scrollTop();
});
return destScrollTop;
}
if(Math.abs(editorScrollTop - lastEditorScrollTop) > 5) {
previewScrollTop = animate(editorScrollTop, mdSectionList, previewElt, htmlSectionList);
}
else if(Math.abs(previewScrollTop - lastPreviewScrollTop) > 5) {
editorScrollTop = animate(previewScrollTop, htmlSectionList, editorElt, mdSectionList);
}
}, 1000);
// Create the layout
core.createLayout = function() {
var layout = undefined;
@ -281,6 +413,9 @@ define(
togglerLength_closed : 90,
stateManagement__enabled : false
};
if(core.settings.scrollLink === true) {
layoutGlobalConfig.onresize = buildSections;
}
if (core.settings.layoutOrientation == "horizontal") {
$(".ui-layout-south").remove();
$(".ui-layout-east").addClass("well").prop("id", "wmd-preview");
@ -311,8 +446,13 @@ define(
$("#navbar").click(function() {
layout.allowOverflow('north');
});
// ScrollLink
if(core.settings.scrollLink === true) {
$("#wmd-input, #wmd-preview").scroll(scrollLink);
}
};
// Create the PageDown editor
var insertLinkCallback = undefined;
core.createEditor = function(onTextChange) {
@ -335,6 +475,11 @@ define(
return text;
});
var editor = new Markdown.Editor(converter);
if(core.settings.scrollLink === true) {
editor.hooks.chain("onPreviewRefresh", function() {
buildSections();
});
}
// Custom insert link dialog
editor.hooks.set("insertLinkDialog", function (callback) {
insertLinkCallback = callback;
@ -709,12 +854,14 @@ define(
core.createLayout();
// Editor's textarea
$("#wmd-input").css({
$("#wmd-input, #md-section-helper").css({
// Apply editor font size
"font-size": core.settings.editorFontSize + "px",
"line-height": Math.round(core.settings.editorFontSize * (20/14)) + "px"
}).keydown(function(e) {
// Manage tab key
});
// Manage tab key
$("#wmd-input").keydown(function(e) {
if(e.keyCode === 9) {
var value = $(this).val();
var start = this.selectionStart;
@ -729,6 +876,45 @@ define(
}
});
// Tooltips
$(".tooltip-scroll-link").tooltip({
html: true,
container: '#modal-settings',
placement: 'right',
title: ['Scroll Link is a feature that binds together editor and preview scrollbars. ',
'It allows you to keep an eye on the preview while scrolling the editor and vice versa. ',
'<br><br>',
'The mapping between Markdown and HTML is based on the position of the title elements (h1, h2, ...) in the page. ',
'Therefore, if your document does not contain any title, the mapping will be linear and consequently less efficient.',
].join("")
});
$(".tooltip-template").tooltip({
html: true,
container: '#modal-settings',
placement: 'right',
trigger: 'manual',
title: ['Available variables:<br>',
'<ul><li><b>documentTitle</b>: document title</li>',
'<li><b>documentMarkdown</b>: document in Markdown format</li>',
'<li><b>documentHTML</b>: document in HTML format</li>',
'<li><b>publishAttributes</b>: attributes of the publish location (undefined when using "Save")</li></ul>',
'Examples:<br>',
_.escape('<title><%= documentTitle %></title>'),
'<br>',
_.escape('<div><%- documentHTML %></div>'),
'<br>',
_.escape('<% if(publishAttributes.provider == "github") print(documentMarkdown); %>'),
'<br><br><a target="_blank" href="http://underscorejs.org/#template">More info</a>',
].join("")
}).click(function(e) {
$(this).tooltip('show');
e.stopPropagation();
});
$(document).click(function(e) {
$(".tooltip-template").tooltip('hide');
});
// Reset inputs
$(".action-reset-input").click(function() {
core.resetModalInputs();

156
js/css_browser_selector.js Normal file
View File

@ -0,0 +1,156 @@
/*
CSS Browser Selector 0.6.1
Originally written by Rafael Lima (http://rafael.adm.br)
http://rafael.adm.br/css_browser_selector
License: http://creativecommons.org/licenses/by/2.5/
Co-maintained by:
https://github.com/verbatim/css_browser_selector
*/
showLog=true;
function log(m) {if ( window.console && showLog ) {console.log(m); } }
function css_browser_selector(u)
{
var uaInfo = {},
screens = [320, 480, 640, 768, 1024, 1152, 1280, 1440, 1680, 1920, 2560],
allScreens = screens.length,
ua=u.toLowerCase(),
is=function(t) { return RegExp(t,"i").test(ua); },
version = function(p,n)
{
n=n.replace(".","_"); var i = n.indexOf('_'), ver="";
while (i>0) {ver += " "+ p+n.substring(0,i);i = n.indexOf('_', i+1);}
ver += " "+p+n; return ver;
},
g='gecko',
w='webkit',
c='chrome',
f='firefox',
s='safari',
o='opera',
m='mobile',
a='android',
bb='blackberry',
lang='lang_',
dv='device_',
html=document.documentElement,
b= [
// browser
(!(/opera|webtv/i.test(ua))&&/msie\s(\d+)/.test(ua))?('ie ie'+(/trident\/4\.0/.test(ua) ? '8' : RegExp.$1))
:is('firefox/')?g+ " " + f+(/firefox\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua)?' '+f+RegExp.$2 + ' '+f+RegExp.$2+"_"+RegExp.$4:'')
:is('gecko/')?g
:is('opera')?o+(/version\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua)?' '+o+RegExp.$2 + ' '+o+RegExp.$2+"_"+RegExp.$4 : (/opera(\s|\/)(\d+)\.(\d+)/.test(ua)?' '+o+RegExp.$2+" "+o+RegExp.$2+"_"+RegExp.$3:''))
:is('konqueror')?'konqueror'
:is('blackberry') ?
( bb +
( /Version\/(\d+)(\.(\d+)+)/i.test(ua)
? " " + bb+ RegExp.$1 + " "+bb+ RegExp.$1+RegExp.$2.replace('.','_')
: (/Blackberry ?(([0-9]+)([a-z]?))[\/|;]/gi.test(ua)
? ' ' +bb+RegExp.$2 + (RegExp.$3?' ' +bb+RegExp.$2+RegExp.$3:'')
: '')
)
) // blackberry
:is('android') ?
( a +
( /Version\/(\d+)(\.(\d+))+/i.test(ua)
? " " + a+ RegExp.$1 + " "+a+ RegExp.$1+RegExp.$2.replace('.','_')
: '')
+ (/Android (.+); (.+) Build/i.test(ua)
? ' '+dv+( (RegExp.$2).replace(/ /g,"_") ).replace(/-/g,"_")
:'' )
) //android
:is('chrome')?w+ ' '+c+(/chrome\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua)?' '+c+RegExp.$2 +((RegExp.$4>0) ? ' '+c+RegExp.$2+"_"+RegExp.$4:''):'')
:is('iron')?w+' iron'
:is('applewebkit/') ?
( w+ ' '+ s +
( /version\/((\d+)(\.(\d+))(\.\d+)*)/.test(ua)
? ' '+ s +RegExp.$2 + " "+s+ RegExp.$2+RegExp.$3.replace('.','_')
: ( / Safari\/(\d+)/i.test(ua)
?
( (RegExp.$1=="419" || RegExp.$1=="417" || RegExp.$1=="416" || RegExp.$1=="412" ) ? ' '+ s + '2_0'
: RegExp.$1=="312" ? ' '+ s + '1_3'
: RegExp.$1=="125" ? ' '+ s + '1_2'
: RegExp.$1=="85" ? ' '+ s + '1_0'
: '' )
:'')
)
) //applewebkit
:is('mozilla/')?g
:''
// mobile
,is("android|mobi|mobile|j2me|iphone|ipod|ipad|blackberry|playbook|kindle|silk")?m:''
// os/platform
,is('j2me')?'j2me'
:is('ipad|ipod|iphone')?
(
(
/CPU( iPhone)? OS (\d+[_|\.]\d+([_|\.]\d+)*)/i.test(ua) ?
'ios' + version('ios',RegExp.$2) : ''
) + ' ' + ( /(ip(ad|od|hone))/gi.test(ua) ? RegExp.$1 : "" )
) //'iphone'
//:is('ipod')?'ipod'
//:is('ipad')?'ipad'
:is('playbook')?'playbook'
:is('kindle|silk')?'kindle'
:is('playbook')?'playbook'
:is('mac')?'mac'+ (/mac os x ((\d+)[.|_](\d+))/.test(ua) ? ( ' mac' + (RegExp.$2) + ' mac' + (RegExp.$1).replace('.',"_") ) : '' )
:is('win')?'win'+
(is('windows nt 6.2')?' win8'
:is('windows nt 6.1')?' win7'
:is('windows nt 6.0')?' vista'
:is('windows nt 5.2') || is('windows nt 5.1') ? ' win_xp'
:is('windows nt 5.0')?' win_2k'
:is('windows nt 4.0') || is('WinNT4.0') ?' win_nt'
: ''
)
:is('freebsd')?'freebsd'
:(is('x11|linux'))?'linux'
:''
// user agent language
,(/[; |\[](([a-z]{2})(\-[a-z]{2})?)[)|;|\]]/i.test(ua))?(lang+RegExp.$2).replace("-","_")+(RegExp.$3!=''?(' '+lang+RegExp.$1).replace("-","_"):''):''
// beta: test if running iPad app
,( is('ipad|iphone|ipod') && !is('safari') ) ? 'ipad_app' : ''
]; // b
function screenSize()
{
var w = window.outerWidth || html.clientWidth;
var h = window.outerHeight || html.clientHeight;
uaInfo.orientation = ((w<h) ? "portrait" : "landscape");
// remove previous min-width, max-width, client-width, client-height, and orientation
html.className = html.className.replace(/ ?orientation_\w+/g, "").replace(/ [min|max|cl]+[w|h]_\d+/g, "")
for (var i=(allScreens-1);i>=0;i--) { if (w >= screens[i] ) { uaInfo.maxw = screens[i]; break; }}
widthClasses="";
for (var info in uaInfo) { widthClasses+=" "+info+"_"+ uaInfo[info] };
html.className = ( html.className +widthClasses );
return widthClasses;
} // screenSize
window.onresize = screenSize;
screenSize();
var cssbs = (b.join(' ')) + " js ";
html.className = ( cssbs + html.className.replace(/\b(no[-|_]?)?js\b/g,"") ).replace(/^ /, "").replace(/ +/g," ");
return cssbs;
}
css_browser_selector(navigator.userAgent);

13
js/main-min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -221,33 +221,6 @@ define(["jquery", "core", "github-provider", "blogger-provider", "dropbox-provid
publisher.publish();
}
});
$(".tooltip-template").tooltip({
html: true,
container: '#modal-settings',
placement: 'right',
trigger: 'manual',
title: ['Available variables:<br>',
'<ul><li><b>documentTitle</b>: document title</li>',
'<li><b>documentMarkdown</b>: document in Markdown format</li>',
'<li><b>documentHTML</b>: document in HTML format</li>',
'<li><b>publishAttributes</b>: attributes of the publish location (undefined when using "Save")</li></ul>',
'Examples:<br>',
_.escape('<title><%= documentTitle %></title>'),
'<br>',
_.escape('<div><%- documentHTML %></div>'),
'<br>',
_.escape('<% if(publishAttributes.provider == "github") print(documentMarkdown); %>'),
'<br><br><a target="_blank" href="http://underscorejs.org/#template">More info</a>',
].join("")
}).click(function(e) {
$(this).tooltip('show');
e.stopPropagation();
});
$(document).click(function(e) {
$(".tooltip-template").tooltip('hide');
});
});
return publisher;