Skip to content

Instantly share code, notes, and snippets.

@westonruter
Created June 15, 2010 17:53
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save westonruter/439429 to your computer and use it in GitHub Desktop.
Save westonruter/439429 to your computer and use it in GitHub Desktop.
Webpage Exposé User Script: Hit Alt+Spacebar to toggle zooming in & out of a page so that everything can be seen at once. When zoomed out, clicking on an element will unzoom and scroll to it. Uses CSS3 Transforms. Tested in Chrome and Firefox 4 w/ GMonkey
// ==UserScript==
// @name Webpage Exposé
// @description Hit Alt+Spacebar to toggle zooming in & out of a page so that everything can be seen at once. When zoomed out, clicking on an element will unzoom and scroll to it. Uses CSS3 Transforms. Tested in Chrome and Firefox 4 w/ Greasemonkey.
// @namespace http://weston.ruter.net/
// @include *
// @license GPL/MIT
// @version 0.2
// @author Weston Ruter (@westonruter) <http://weston.ruter.net/> of X-Team <http://x-team.com/>
// ==/UserScript==
/**
* ## Todo ##
* - Allow the exposé to auto-undo if the keyup happens long enough after the keydown: allow it to stick like OS X?
* - The expose feature could be activated via Shift+Scrollup; allow for incremental zooming
* - Rescale when resizing?
* - If toggling during isTransition, either stop and reverse current transition (best) or add the toggle to the queue upon transitionend
*/
(function(window, document, console){
var transitionDuration = 500; //ms
var prevScrollOffset = {
top: 0,
left: 0
};
var isExposed = false;
var isTransitioning = false;
/**
* Toggle when Shift+Spacebar hit
*/
document.addEventListener('keypress', function(e){
if(!isTransitioning && e.altKey && (e.keyCode || e.charCode) == 160){ // Alt + Space
e.preventDefault();
if(!isExposed){
exposePage();
}
else {
unexposePage();
}
}
}, false);
/**
* When scaling is enabled, clicking on an element will undo scaling and scroll to it
*/
document.addEventListener('click', function(e){
if(!isTransitioning && isExposed){
unexposePage(true /*skipScrollOffsetReset*/);
var scale = getCurrentScale();
scrollTopDuringZoom(e.clientY*(1/scale));
e.preventDefault();
}
}, false);
/**
* Scale down the page with a CSS3 Transform so that everything is visible
*/
function exposePage(){
if(document.documentElement.scrollHeight > window.innerHeight || document.documentElement.scrollWidth > window.innerWidth){
// Save our previous scroll position and then go to the top
prevScrollOffset.left = document.documentElement.scrollLeft || document.body.scrollLeft;
prevScrollOffset.top = document.documentElement.scrollTop || document.body.scrollTop;
// This is equivalent to document.documentElement.scrollHeight in Chrome only.
// In Firefox, window.innerHeight == document.documentElement.scrollHeight
var pageRect = document.documentElement.getBoundingClientRect();
var scale = Math.min(
window.innerHeight / document.documentElement.scrollHeight,
window.innerWidth / document.documentElement.scrollWidth
);
var style = document.documentElement.style;
setVendorProperty(style, 'transitionDuration', transitionDuration + 'ms');
setVendorProperty(style, 'transitionProperty', 'transform, margin', true);
setVendorProperty(style, 'transform', 'scale(' + scale + ')');
// Cancel out the padding introduced by the scaling transform
var margin = Math.floor(-pageRect.height * ((1-scale)/2));
style.marginBottom = margin + 'px';
style.marginTop = margin + 'px';
style.overflowY = 'scroll';
isExposed = true;
isTransitioning = true;
}
}
/**
* Undo any scaling (revert)
* @param {Boolean} skipScrollOffsetReset
*/
function unexposePage(skipScrollOffsetReset){
isTransitioning = true;
if(!skipScrollOffsetReset){
scrollTopDuringZoom(prevScrollOffset.top);
}
var style = document.documentElement.style;
setVendorProperty(style, 'transform', '');
style.marginTop = '';
style.marginBottom = '';
style.overflowY = '';
isExposed = false;
}
/**
* Function which gets called when the zoomout is finished; this tells
* scrollTopDuringZoom() to stop doing its thing.
* @param {Object} transition event
*/
function handleTransitionEnd(e){
isTransitioning = false;
}
var transitionEndVendorNames = [
'transitionend',
'OTransitionEnd',
'webkitTransitionEnd'
];
transitionEndVendorNames.forEach(function(eventName){
document.documentElement.addEventListener(eventName, handleTransitionEnd, true);
});
/**
* Get the transform's computed scale, including during transition
* @returns {Number}
*/
function getCurrentScale(){
var style = window.getComputedStyle(document.documentElement, null);
var matrix;
var propertyNames = prefixNameAll('transform', true);
for(var i = 0, len = propertyNames.length; i < len; i++){
matrix = style[propertyNames[i]];
if(matrix){
var matrixMatch = matrix.match(/matrix\s*\(\s*(\d+(?:.\d+)?)/);
return matrixMatch ? parseFloat(matrixMatch[1], 10) : 1.0;
}
}
throw Error('Unable to get the computed style for the transform property.');
}
/**
* Keep scrolling to a certain position while the page is zoomed in (the scale inreases)
* @param {Number} top
*/
function scrollTopDuringZoom(top){
function checkScale(){
var scale = isTransitioning ? getCurrentScale() : 1.0;
window.scrollTo(0, scale*top);
if(isTransitioning){
(
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.requestAnimationFrame
)(checkScale);
}
}
checkScale();
}
var vendorPrefixes = [
'webkit',
'moz',
'ms',
'o'
];
/**
* Prefix a name with a single prefix
* @param {String} prefix
* @param {String} name
* @param {Boolean} isCamelCased
* @returns {Array}
*/
function setVendorProperty(style, name, value, isValuePrefixed){
vendorPrefixes.forEach(function(prefix){
var propertyName = prefixName(prefix, name, true);
var propertyValue = value;
if(isValuePrefixed){
propertyValue = prefixName(prefix, propertyValue, false);
}
style[propertyName] = propertyValue;
});
style[name] = value;
}
/**
* Prefix a name with a single prefix
* @param {String} prefix
* @param {String} name
* @param {Boolean} isCamelCased
* @returns {Array}
*/
function prefixName(prefix, name, isCamelCased){
if(isCamelCased && name.indexOf('-') != -1){
throw Error('Currently no support for camcel-casing sub-properties like ' + name);
}
if(isCamelCased){
return prefix.substr(0,1).toUpperCase()+prefix.substr(1) + name.substr(0,1).toUpperCase()+name.substr(1);
}
else {
return '-' + prefix + '-' + name;
}
}
/**
* Prefix a name with all prefixes
* @param {String} prefix
* @param {String} name
* @param {Boolean} isCamelCased
* @returns {Array}
*/
function prefixNameAll(name, isCamelCased){
var props = [];
vendorPrefixes.forEach(function(prefix){
props.push( prefixName(prefix, name, isCamelCased) );
});
props.push(name);
return props;
}
})(
window,
document,
(typeof unsafeWindow == 'undefined' ? window : unsafeWindow).console
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment