Created
June 15, 2010 17:53
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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