User:Matma Rex/article-map.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*!
* Proof-of-concept of an "article map" sidebar for Wikipedia articles.
* Displays a scaled-down version of the page on the right side of the window.
* A button will appear in the top-right corner, clicking it will enable the map.
*
* Doesn't work correctly on Chrome (and Chrome-like browsers) because of its
* numerous bugs, doesn't work on Internet Explorer because of its lack of
* support for advanced SVG features.
*
* Works on Firefox (but gets laggy for *very* long articles), works on Opera 12
* (but gets laggy for long articles).
*
* To enable, add the following snippet to your common.js page (on any wiki):
* mw.loader.load('//meta.wikimedia.org/w/index.php?title=User:Matma_Rex/article-map.js&action=raw&ctype=text/javascript');
*
* If you're reckless and don't mind your browser possibly locking up from time
* to time, and want to always display the map immediately when a page is loaded,
* also add this snippet:
* window.articleMapRenderOnLoad = true;
*
* Author: Bartosz Dziewoński ([[User:Matma Rex]])
* Released under the terms of the MIT license.
* Little attention was paid to details and code quality. Beware.
* version 1.1
*/
/*global $, mw */
( function () {
var sidebarWidth = 150;
var displaying = false;
var loadPromise = $.Deferred();
if(document.readyState === "complete") {
loadPromise.resolve();
} else {
$(window).on('load', function () {
loadPromise.resolve();
});
}
var contentWidth, scale, contentHeight, sidebarHeight, windowHeight, contentTop, contentBottom,
maxContentScrollTop, needSidebarScrolling, needSidebarBottomOffset, maxSidebarScrollTop;
var $sidebar, $indicator, $svg, $closeButton, $setupButton;
function renderSidebar() {
if(displaying) return;
displaying = true;
// mw.util.addCSS( '#content { transition: margin-right 300ms }' );
// mw.util.addCSS( '#content-sidebar { transition: width 300ms }' );
$setupButton.remove();
$('#content')
.css('margin-right', sidebarWidth);
contentWidth = $('#content').outerWidth();
scale = sidebarWidth / contentWidth;
contentHeight = $('#content').outerHeight();
sidebarHeight = contentHeight * scale;
windowHeight = $(window).height();
contentTop = $('#content').offset().top;
contentBottom = contentTop + contentHeight - windowHeight;
maxContentScrollTop = document.documentElement.scrollHeight - windowHeight;
needSidebarScrolling = sidebarHeight > windowHeight;
needSidebarBottomOffset = sidebarHeight > maxContentScrollTop - contentBottom;
maxSidebarScrollTop = sidebarHeight - windowHeight;
if(scale > 1.0) {
// duh
$('#content')
.css('margin-right', '');
return;
}
$sidebar = $( '<div>' )
.attr('id', 'content-sidebar')
.css({
position: 'fixed',
overflow: 'hidden',
background: 'white',
top: '0',
bottom: '0',
right: '0',
width: sidebarWidth
})
.insertAfter('#content')
.html(
'<svg width="'+sidebarWidth+'" height="'+sidebarHeight+'">' +
'<g transform="scale('+scale+')">' +
'<foreignObject width="'+contentWidth+'" height="'+contentHeight+'">' +
'</foreignObject>' +
'</g>' +
'</svg>'
);
$sidebar.find( 'foreignObject' ).append(
$('#content').clone().attr('id', '').css({
'margin-right': 0,
'margin-left': 0
})
);
$svg = $sidebar.find('svg');
// overlay to prevent clicking and hover effects
var $overlay = $('<div>').css({
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0
});
// overlay indicator showing current scroll position
$indicator = $('<div>').css({
position: 'absolute',
background: 'rgba(0,0,0,0.3)',
left: 0,
top: 0,
width: sidebarWidth,
height: windowHeight * scale
});
$closeButton = $('<div>').text('×').css({
position: 'absolute',
background: 'red',
color: 'white',
right: 0,
top: 0,
width: '1em',
height: '1em',
'text-align': 'center',
'line-height': '1em'
}).on('click', destroySidebar);
$sidebar.append($overlay, $indicator, $closeButton);
function clickHandler(e) {
if(e.target == $closeButton[0]) return;
e.stopPropagation();
e.preventDefault();
var ycoord = sidebarScrollTop + e.clientY;
var scrollTop = (ycoord / scale) - windowHeight/2;
$(document.documentElement).animate({ scrollTop: scrollTop }, 'fast');
scrollHandler();
}
$sidebar[0].addEventListener( 'click', clickHandler, true );
scrollHandler();
}
function destroySidebar() {
if(!displaying) return;
displaying = false;
$('#content')
.css('margin-right', '');
$sidebar.remove();
setupButton();
}
function setupButton() {
$setupButton = $('<button>')
.text('Render minimap')
.css({
position: 'absolute',
bottom: '-2em',
right: '0',
height: '2em'
})
.on('click', function () {
loadPromise.done(renderSidebar);
})
.appendTo('#right-navigation');
}
$( setupButton );
loadPromise.done( function () {
if(window.articleMapRenderOnLoad === true) {
renderSidebar();
}
} );
var state = 'top'; // || 'middle' || 'bottom'
var sidebarScrollTop = 0;
function scrollHandler() {
if(!displaying) return;
if(needSidebarScrolling && needSidebarBottomOffset && document.documentElement.scrollTop > contentBottom) {
// ewwww
$svg.css('margin-top', 0 );
$indicator.css('margin-top', 0 );
} else if(needSidebarScrolling ) {
var scrollPercentage = document.documentElement.scrollTop / maxContentScrollTop;
sidebarScrollTop = scrollPercentage * maxSidebarScrollTop;
$svg.css('margin-top', -sidebarScrollTop );
$indicator.css('margin-top', -sidebarScrollTop );
}
$indicator.css('top', document.documentElement.scrollTop * scale);
if(document.documentElement.scrollTop < contentTop) {
setTimeout( function () {
var top = Math.max(0, contentTop - document.documentElement.scrollTop);
$sidebar.css('top', top);
state = (top ? 'top' : 'middle');
}, 50 );
} else if(needSidebarBottomOffset && document.documentElement.scrollTop > contentBottom) {
setTimeout( function () {
var bottom = Math.max(0, document.documentElement.scrollTop - contentBottom);
$sidebar.css('bottom', bottom);
// this condition is particularly messed up, surely we could do better?
var shiftBy = (document.documentElement.scrollTop - contentTop - contentHeight + sidebarHeight);
$sidebar.css('margin-top', -Math.max(0, shiftBy));
state = (bottom || shiftBy ? 'bottom' : 'middle');
}, 50 );
} else {
if(state !== 'middle') {
$sidebar.css('top', 0);
$sidebar.css('bottom', 0);
$sidebar.css('margin-top', 0);
state = 'middle';
}
}
}
$(window).on('scroll', scrollHandler);
mw.loader.using('mediawiki.util', function () {
$(window).on('resize', mw.util.debounce(100, function () {
if(!displaying) return;
destroySidebar();
renderSidebar();
}));
});
} )();