/* Detect-zoom
* -----------
* Cross Browser Zoom and Pixel Ratio Detector
* Version 1.0.4 | Apr 1 2013
* dual-licensed under the WTFPL and MIT license
* Maintained by https://github/tombigel
* Original developer https://github.com/yonran
*/
export class DetectZoom {
constructor(private window: Window) {}
/**
* Use devicePixelRatio if supported by the browser
*/
get devicePixelRatio(): number {
return this.window.devicePixelRatio || 1;
}
/**
* Fallback function to set default values
*/
private fallback() {
return {
zoom: 1,
devicePxPerCssPx: 1,
};
}
/**
* IE 8 and 9: no trick needed!
*/
private ie8() {
const zoom = Math.round(((this.window.screen as any).deviceXDPI / (this.window.screen as any).logicalXDPI) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * this.devicePixelRatio,
};
}
/**
* For IE10 we need to change our technique again...
* thanks https://github.com/stefanvanburen
*/
private ie10() {
const zoom = Math.round((this.window.document.documentElement.offsetHeight / this.window.innerHeight) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * this.devicePixelRatio,
};
}
/**
* For chrome
*/
private chrome() {
const zoom = Math.round((this.window.outerWidth / this.window.innerWidth) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * this.devicePixelRatio,
};
}
/**
* For safari (same as chrome)
*/
private safari() {
const zoom = Math.round((this.window.document.documentElement.clientWidth / this.window.innerWidth) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * this.devicePixelRatio,
};
}
/**
* Mobile WebKit
* the trick: window.innerWIdth is in CSS pixels, while
* screen.width and screen.height are in system pixels.
* And there are no scrollbars to mess up the measurement.
*/
private webkitMobile() {
const deviceWidth = Math.abs(this.window.screen.orientation.angle) === 90 ? screen.height : screen.width;
const zoom = deviceWidth / window.innerWidth;
return {
zoom: zoom,
devicePxPerCssPx: zoom * this.devicePixelRatio,
};
}
/**
* Desktop Webkit
* the trick: an element's clientHeight is in CSS pixels, while you can
* set its line-height in system pixels using font-size and
* -webkit-text-size-adjust:none.
* device-pixel-ratio: http://www.webkit.org/blog/55/high-dpi-web-sites/
*
* Previous trick (used before http://trac.webkit.org/changeset/100847):
* documentElement.scrollWidth is in CSS pixels, while
* document.width was in system pixels. Note that this is the
* layout width of the document, which is slightly different from viewport
* because document width does not include scrollbars and might be wider
* due to big elements.
*/
private webkit() {
const important = str => str.replace(/;/g, ' !important;');
const div = this.window.document.createElement('div');
div.innerHTML = '1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>0';
div.setAttribute(
'style',
important(
'font: 100px/1em sans-serif; -webkit-text-size-adjust: none; text-size-adjust: none; height: auto; width: 1em; padding: 0; overflow: visible;'
)
);
// The container exists so that the div will be laid out in its own flow
// while not impacting the layout, viewport size, or display of the
// webpage as a whole.
// Add !important and relevant CSS rule resets
// so that other rules cannot affect the results.
const container = this.window.document.createElement('div');
container.setAttribute('style', important('width:0; height:0; overflow:hidden; visibility:hidden; position: absolute;'));
container.appendChild(div);
document.body.appendChild(container);
let zoom = 1000 / div.clientHeight;
zoom = Math.round(zoom * 100) / 100;
document.body.removeChild(container);
return {
zoom: zoom,
devicePxPerCssPx: zoom * this.devicePixelRatio,
};
}
/**
* no real trick; device-pixel-ratio is the ratio of device dpi / css dpi.
* (Note that this is a different interpretation than Webkit's device
* pixel ratio, which is the ratio device dpi / system dpi).
*
* Also, for Mozilla, there is no difference between the zoom factor and the device ratio.
*/
private firefox4() {
const zoom = Math.round(this.mediaQueryBinarySearch('min--moz-device-pixel-ratio', '', 0, 10, 20, 0.0001) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom,
};
}
/**
* Firefox 18.x
* Mozilla added support for devicePixelRatio to Firefox 18,
* but it is affected by the zoom level, so, like in older
* Firefox we can't tell if we are in zoom mode or in a device
* with a different pixel ratio
*/
private firefox18() {
return {
zoom: this.firefox4().zoom,
devicePxPerCssPx: this.devicePixelRatio,
};
}
/**
* works starting Opera 11.11
* the trick: outerWidth is the viewport width including scrollbars in
* system px, while innerWidth is the viewport width including scrollbars
* in CSS px
*/
private opera11() {
const zoom = Math.round((this.window.top.outerWidth / this.window.top.innerWidth) * 100) / 100;
return {
zoom: zoom,
devicePxPerCssPx: zoom * this.devicePixelRatio,
};
}
binarySearch(property: string, unit: string, a: number, b: number, maxIter: number, epsilon: number, matchMedia: any) {
const mid = (a + b) / 2;
if (maxIter <= 0 || b - a < epsilon) {
return mid;
}
const query = '(' + property + ':' + mid + unit + ')';
if (matchMedia(query).matches) {
return this.binarySearch(property, unit, mid, b, maxIter - 1, epsilon, matchMedia);
} else {
return this.binarySearch(property, unit, a, mid, maxIter - 1, epsilon, matchMedia);
}
}
/**
* Use a binary search through media queries to find zoom level in Firefox
*/
private mediaQueryBinarySearch(property: string, unit: string, a: number, b: number, maxIter: number, epsilon: number) {
let matchMedia;
let head, style, div;
if (window.matchMedia) {
matchMedia = window.matchMedia;
} else {
head = document.getElementsByTagName('head')[0];
style = document.createElement('style');
head.appendChild(style);
div = document.createElement('div');
div.className = 'mediaQueryBinarySearch';
div.style.display = 'none';
document.body.appendChild(div);
matchMedia = function (query) {
style.sheet.insertRule('@media ' + query + '{.mediaQueryBinarySearch ' + '{text-decoration: underline} }', 0);
const matched = getComputedStyle(div, null).textDecoration === 'underline';
style.sheet.deleteRule(0);
return { matches: matched };
};
}
const ratio = this.binarySearch(property, unit, a, b, maxIter, epsilon, matchMedia);
if (div) {
head.removeChild(style);
document.body.removeChild(div);
}
return ratio;
}
private detect() {
//IE8+
if (!isNaN((this.window.screen as any).logicalXDPI) && !isNaN((this.window.screen as any).systemXDPI)) {
return this.ie8();
}
// IE10+ / Touch
else if (this.window.navigator.msMaxTouchPoints) {
return this.ie10();
}
//chrome
else if (!!(this.window as any).chrome && !(!!(this.window as any).opera || this.window.navigator.userAgent.indexOf(' Opera') >= 0)) {
return this.chrome();
}
//safari
else if (Object.prototype.toString.call((this.window as any).HTMLElement).indexOf('Constructor') > 0) {
return this.safari();
}
//Mobile Webkit
else if ('orientation' in this.window && 'webkitRequestAnimationFrame' in this.window) {
return this.webkitMobile();
}
//WebKit
else if ('webkitRequestAnimationFrame' in this.window) {
return this.webkit();
}
//Opera
else if ((this.window as any).navigator.userAgent.indexOf('Opera') >= 0) {
return this.opera11();
}
//Last one is Firefox
//FF 18.x
else if ((this.window as any).devicePixelRatio) {
return this.firefox18();
}
//FF 4.0 - 17.x
else if (this.firefox4().zoom > 0.001) {
return this.firefox4();
}
return this.fallback();
}
zoom() {
return this.detect().zoom;
}
/**
* Ratios.devicePxPerCssPx shorthand
*/
device() {
return this.detect().devicePxPerCssPx;
}
}