1 // detect-zoom is dual-licensed under the WTFPL and MIT license,
2 // at the recipient's choice.
3 // https://github.com/yonran/detect-zoom/
5 mediaQueryBinarySearch: function (
6 property, unit, a, b, maxIter, epsilon) {
9 if (window.matchMedia) {
10 matchMedia = window.matchMedia;
12 head = document.getElementsByTagName('head')[0];
13 style = document.createElement('style');
14 div = document.createElement('div');
15 div.className = 'mediaQueryBinarySearch';
16 head.appendChild(style);
17 div.style.display = 'none';
18 document.body.appendChild(div);
19 matchMedia = function (query) {
20 style.sheet.insertRule('@media ' + query +
21 '{.mediaQueryBinarySearch ' +
22 '{text-decoration: underline} }', 0);
23 var matched = getComputedStyle(div, null).textDecoration
25 style.sheet.deleteRule(0);
26 return { matches: matched };
29 var r = binarySearch(a, b, maxIter);
31 head.removeChild(style);
32 document.body.removeChild(div);
36 function binarySearch(a, b, maxIter) {
37 var mid = (a + b) / 2;
38 if (maxIter == 0 || b - a < epsilon) return mid;
39 var query = "(" + property + ":" + mid + unit + ")";
40 if (matchMedia(query).matches) {
41 return binarySearch(mid, b, maxIter - 1);
43 return binarySearch(a, mid, maxIter - 1);
47 _zoomIe7: function () {
48 // the trick: body's offsetWidth was in CSS pixels, while
49 // getBoundingClientRect() was in system pixels in IE7.
50 // Thanks to http://help.dottoro.com/ljgshbne.php
51 var rect = document.body.getBoundingClientRect();
52 var z = (rect.right - rect.left) / document.body.offsetWidth;
53 z = Math.round(z * 100) / 100;
54 return { zoom: z, devicePxPerCssPx: z };
56 _zoomIe8: function () {
57 // IE 8+: no trick needed!
58 // TODO: MSDN says that logicalXDPI and deviceXDPI existed since IE6
59 // (which didn't even have whole-page zoom). Check to see whether
60 // this method would also work in IE7.
61 var zoom = screen.deviceXDPI / screen.logicalXDPI;
64 devicePxPerCssPx: zoom
67 _zoomWebkitMobile: function () {
68 // the trick: window.innerWIdth is in CSS pixels, while
69 // screen.width and screen.height are in system pixels.
70 // And there are no scrollbars to mess up the measurement.
71 var devicePixelRatio = window.devicePixelRatio != null ? window.devicePixelRatio : 1
73 if (Math.abs(window.orientation) == 90) {
74 deviceWidth = screen.height;
76 deviceWidth = screen.width;
78 var z = deviceWidth / window.innerWidth;
79 // return immediately; don't round at the end.
80 return { zoom: z, devicePxPerCssPx: z * devicePixelRatio };
82 _zoomWebkit: function () {
83 // the trick: an element's clientHeight is in CSS pixels, while you can
84 // set its line-height in system pixels using font-size and
85 // -webkit-text-size-adjust:none.
86 // device-pixel-ratio: http://www.webkit.org/blog/55/high-dpi-web-sites/
88 // Previous trick (used before http://trac.webkit.org/changeset/100847):
89 // documentElement.scrollWidth is in CSS pixels, while
90 // document.width was in system pixels. Note that this is the
91 // layout width of the document, which is slightly different from viewport
92 // because document width does not include scrollbars and might be wider
93 // due to big elements.
95 var devicePixelRatio = window.devicePixelRatio != null ? window.devicePixelRatio : 1;
97 // The container exists so that the div will be laid out in its own flow
98 // while not impacting the layout, viewport size, or display of the
99 // webpage as a whole.
100 var container = document.createElement('div')
101 , div = document.createElement('div');
103 // Add !important and relevant CSS rule resets
104 // so that other rules cannot affect the results.
105 var important = function (str) { return str.replace(/;/g, " !important;"); };
107 container.setAttribute('style', important('width:0; height:0; overflow:hidden; visibility:hidden; position: absolute;'));
108 div.innerHTML = "1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>0";
109 div.setAttribute('style', important('font: 100px/1em sans-serif; -webkit-text-size-adjust: none; height: auto; width: 1em; padding: 0; overflow: visible;'));
111 container.appendChild(div);
112 document.body.appendChild(container);
113 var z = 1000 / div.clientHeight;
114 z = Math.round(z * 100) / 100;
117 devicePxPerCssPx: devicePixelRatio * z
119 document.body.removeChild(container);
122 _zoomFF35: function () {
123 // the trick for FF3.5 ONLY: device-width gives CSS pixels, while
124 // screen.width gave system pixels. Thanks to QuirksMode's widths table,
125 // which called it a bug. http://www.quirksmode.org/m/widths.html
126 var z = screen.width /
127 this.mediaQueryBinarySearch('min-device-width', 'px', 0, 6000, 20, .0001);
128 z = Math.round(z * 100) / 100;
129 return { zoom: z, devicePxPerCssPx: z };
131 _zoomFF36: function () {
132 // the hack for FF3.6: you can measure scrollbar's width in CSS pixels,
133 // while in system pixels it's 15px (verified in Ubuntu).
135 // TODO: verify for every platform that a scrollbar is exactly 15px wide.
136 var container = document.createElement('div')
137 , outerDiv = document.createElement('div');
138 // The container exists so that the div will be laid out in its own flow
139 // while not impacting the layout, viewport size, or display of the
140 // webpage as a whole.
141 container.setAttribute('style', 'width:0; height:0; overflow:hidden;' +
142 'visibility:hidden; position: absolute');
143 outerDiv.style.width = outerDiv.style.height = '500px'; // enough for all the scrollbars
145 for (var i = 0; i < 10; ++i) {
146 var child = document.createElement('div');
147 child.style.overflowY = 'scroll';
148 div.appendChild(child);
151 container.appendChild(outerDiv);
152 document.body.appendChild(container);
153 var outerDivWidth = outerDiv.clientWidth;
154 var innerDivWidth = div.clientWidth;
155 var scrollbarWidthCss = (outerDivWidth - innerDivWidth) / 10;
156 document.body.removeChild(container);
157 var scrollbarWidthDevice = 15; // Mac and Linux: scrollbars are 15px wide
158 if (-1 != navigator.platform.indexOf('Win')) {
159 scrollbarWidthDevice = 17;
161 var z = scrollbarWidthDevice / scrollbarWidthCss;
162 z = Math.round(z * 100) / 100;
163 return { zoom: z, devicePxPerCssPx: z };
165 _zoomFF4: function () {
166 // no real trick; device-pixel-ratio is the ratio of device dpi / css dpi.
167 // (Note that this is a different interpretation than Webkit's device
168 // pixel ratio, which is the ratio device dpi / system dpi).
169 // TODO: is mozmm vs. mm promising?
170 var z = this.mediaQueryBinarySearch(
171 'min--moz-device-pixel-ratio',
172 '', 0, 10, 20, .0001);
173 z = Math.round(z * 100) / 100;
174 return { zoom: z, devicePxPerCssPx: z };
176 _zoomOperaOlder: function () {
177 // 10.00 (or before) to 11.01:
178 // the trick: a div with position:fixed;width:100%'s offsetWidth is the
179 // viewport width in CSS pixels, while window.innerWidth was in system
180 // pixels. Thanks to:
181 // http://virtuelvis.com/2005/05/how-to-detect-zoom-level-in-opera/
182 // TODO: fix bug: when there is a scrollbar, fixed div does NOT
183 // include the scrollbar, while window.outerWidth DOES. This causes the
184 // calculation to be off by a few percent.
185 var fixedDiv = document.createElement('div');
186 fixedDiv.style.position = 'fixed';
187 fixedDiv.style.width = '100%';
188 fixedDiv.style.height = '100%';
189 fixedDiv.style.top = fixedDiv.style.left = '0';
190 fixedDiv.style.visibility = 'hidden';
191 document.body.appendChild(fixedDiv);
192 var z = window.innerWidth / fixedDiv.offsetWidth;
193 document.body.removeChild(fixedDiv);
194 return { zoom: z, devicePxPerCssPx: z };
196 _zoomOpera11: function () {
197 // works starting Opera 11.11
198 // the trick: outerWidth is the viewport width including scrollbars in
199 // system px, while innerWidth is the viewport width including scrollbars
201 var z = window.outerWidth / window.innerWidth;
202 z = Math.round(z * 100) / 100;
203 return { zoom: z, devicePxPerCssPx: z };
205 ratios: function () {
207 if (!isNaN(screen.logicalXDPI) && !isNaN(screen.systemXDPI)) {
208 return this._zoomIe8();
209 } else if ('ontouchstart' in window && document.body.style.webkitTextSizeAdjust != null) {
210 return this._zoomWebkitMobile();
211 } else if (document.body.style.webkitTextSizeAdjust != null) { // webkit
212 return this._zoomWebkit();
213 } else if (-1 != navigator.userAgent.indexOf('Firefox/3.5')) {
214 return this._zoomFF35();
215 } else if (-1 != navigator.userAgent.indexOf('Firefox/3.6')) {
216 return this._zoomFF36();
217 } else if (-1 != navigator.appVersion.indexOf("MSIE 7.")) {
218 return this._zoomIe7();
219 } else if (-1 != navigator.userAgent.indexOf('Opera')) {
220 var versionIdx = navigator.userAgent.indexOf('Version/');
221 if (11.01 < parseFloat(navigator.userAgent.substr(versionIdx + 8)))
222 return this._zoomOpera11();
224 return this._zoomOperaOlder();
225 } else if (0.001 < (r = this._zoomFF4()).zoom) {
228 return { zoom: 1, devicePxPerCssPx: 1 }
232 return this.ratios().zoom;
234 device: function () {
235 return this.ratios().devicePxPerCssPx;