├── .gitignore ├── Makefile ├── README.md ├── detect-zoom.js ├── detect-zoom.min.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Python files 2 | *.pyc 3 | 4 | # Folder view configuration files 5 | .DS_Store 6 | Desktop.ini 7 | 8 | # Thumbnail cache files 9 | ._* 10 | Thumbs.db 11 | 12 | # Files that might appear on external disks 13 | .Spotlight-V100 14 | .Trashes 15 | 16 | # IntelliJ 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea 21 | 22 | # npm 23 | node_modules -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UGLIFY=./node_modules/uglify-js/bin/uglifyjs 2 | 3 | detect-zoom.min.js: detect-zoom.js 4 | $(UGLIFY) detect-zoom.js -c > detect-zoom.min.js 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cross Browser Zoom and Pixel Ratio Detector 2 | ====== 3 | ------ 4 | 5 | ### READ THIS: Detect-zoom is currently unusable for desktop 6 | 7 | Last update: Aug 7 2013 8 | 9 | **In the past few months both Mozilla and Google made some changes to their browsers that make it almost impossible to do 10 | what detect-zoom is here to do:** 11 | 12 | #### Firefox 13 | On *Firefox 18* Mozilla changes the `devicePixelRatio` value on manual zoom (cmd/ctrl +/-), making it impossible 14 | to know whether the browser is in zoom mode or is it a retina device, ignoring what the word DEVICE represents. 15 | I personally believe someone there refuses to admit this is a mistake and revert this decision. 16 | 17 | #### Chrome 18 | On *Chrome 27* (Meaning WebKit and Blink) `webkitTextSizeAdjust` was deprecated on desktops versions of the browser. 19 | This was the only bullet proof way to detect zoom in desktop chrome that I am aware of. 20 | There are couple of other ways, but they don't cover all the bases - one uses SVG but is not working in iFrames, the other 21 | uses window.inner/outerWidth and is not working when there is a sidebar or the DevTools are open on the side. 22 | 23 | ### Other Known issues: 24 | * In some multi-monitor enviroments where each monitor has a different 'pixel aspect ratio' windows that span accross both monitors might return false pixelAspectRatio values. 25 | 26 | What is this for? 27 | ------ 28 | Detecting the browser zoom level and device pixel ratio relative to the zoom level. 29 | 30 | It can be used to show higher-resolution `canvas` or `img` when necessary, 31 | to warn users that your site's layout will be broken in their current zoom level, 32 | and much more. 33 | Personally I'm maintaining it to use Detect-zoom in [Wix.com](http://wix.com)'s editor to warn users 34 | that their browser is in zoom mode before saving important changes to their website. 35 | 36 | What happend to @yonran? 37 | ------ 38 | Don't worry, he is well. 39 | As of January 2013 [@yonran](https://github.com/yonran) stopped maintaining his source of detect-zoom, and transferred the repository to me. 40 | If you are looking to update previous versions note that there were some breaking changes 41 | 42 | * **Major Changes from the latest yonran version:** 43 | * `DetectZoom` object name changed to `detectZoom` 44 | * `DetectZoom.ratio()` is no longer publicly accessible 45 | * Supported browsers: IE8+, FF4+, modern Webkit, mobile Webkit, Opera 11.1+ 46 | * *IE6, IE7, FF 3.6 and Opera 10.x are no longer supported* 47 | * Added support to be loaded as an AMD and CommonJS module 48 | 49 | Live Example 50 | ------ 51 | See the Live Example section in 52 | http://tombigel.github.com/detect-zoom/ 53 | 54 | Usage 55 | ------ 56 | **Detect-zoom has only two public functions:** 57 | * `zoom()` Returns the zoom level of the user's browser using Javascript. 58 | * `device()` Returns the device pixel ratio multiplied by the zoom level (Read [more about devicePixelRatio](http://www.quirksmode.org/blog/archives/2012/07/more_about_devi.html) at QuirksMode) 59 | 60 | ```html 61 | 62 | 68 | ``` 69 | 70 | **AMD Usage** 71 | 72 | ```javascript 73 | require(['detect-zoom'], function(detectZoom){ 74 | var zoom = detectZoom.zoom(); 75 | }); 76 | ``` 77 | 78 | **Installing with NPM** 79 | 80 | ```bash 81 | > npm install detect-zoom 82 | ``` 83 | 84 | Changelog 85 | ------ 86 | 87 | 2013/4/01 88 | * Changed WebKit detection from deprecated webkitTextSizeAdjust to webkitMarquee 89 | * Changed WebKitMobile detection from unreliable 'ontouchstart' event (can be triggered on desktops too) to 'orientation' property that is hopefully more reliable 90 | * Minor version bump to 1.0.4 91 | 92 | 2013/3/29 93 | * Added package.json (thanks [@TCampaigne](https://github.com/TCampaigne)) 94 | * Some documentation fixes 95 | * Added detect-zoom to npm package manager (again, thanks [@TCampaigne](https://github.com/TCampaigne)) 96 | 97 | 2013/2/25 98 | * Fixed a missing 'else' between ie8 and ie10 detection 99 | * Minor version bump to 1.0.2 100 | 101 | 2013/2/15 102 | * Added a fix for IE10 Metro (or whatever MS calls it these days..) by [@stefanvanburen](https://github.com/stefanvanburen) 103 | * Minor version bump to 1.0.1 104 | * Added minimized version 105 | 106 | 2013/2/05 107 | * Merged a pull request that fixed zoom on IE being returned X100 (thanks [@kreymerman](https://github.com/kreymerman)) 108 | * Refactored the code some more, changed some function names 109 | * Browser dependent main function is created only on initialization (thanks [@jsmaker](https://github.com/jsmaker)) 110 | * _Open Issue: Firefox returns `zoom` and `devicePixelRatio` the same. Still looking for a solution here._ 111 | * Started versioning - this is version 1.0.0 112 | 113 | 2013/1/27 114 | * Added a fix to Mozilla's (Broken as I see it - https://bugzilla.mozilla.org/show_bug.cgi?id=809788) 115 | implementation of window.devicePixel starting Firefox 18 116 | 117 | 2013/1/26 118 | * Repository moved here 119 | * Refactored most of the code 120 | * Removed support for older browsers 121 | * Added support for AMD and CommonJS 122 | 123 | 124 | Help Needed 125 | ------ 126 | 127 | ***Detect-zoom is not complete, many parts of the code are 6 to 12 months old and I'm still reviewing them 128 | I need help testing different browsers, finding better ways to measure zoom on problematic browsers (ahm.. Firefox.. ahm) 129 | patches are more than welcome.*** 130 | 131 | 132 | License 133 | ------ 134 | 135 | Detect-zoom is dual-licensed under the [WTFPL](http://www.wtfpl.net/about/) and [MIT](http://opensource.org/licenses/MIT) license, at the recipient's choice. 136 | -------------------------------------------------------------------------------- /detect-zoom.js: -------------------------------------------------------------------------------- 1 | /* Detect-zoom 2 | * ----------- 3 | * Cross Browser Zoom and Pixel Ratio Detector 4 | * Version 1.0.4 | Apr 1 2013 5 | * dual-licensed under the WTFPL and MIT license 6 | * Maintained by https://github/tombigel 7 | * Original developer https://github.com/yonran 8 | */ 9 | 10 | //AMD and CommonJS initialization copied from https://github.com/zohararad/audio5js 11 | (function (root, ns, factory) { 12 | "use strict"; 13 | 14 | if (typeof (module) !== 'undefined' && module.exports) { // CommonJS 15 | module.exports = factory(ns, root); 16 | } else if (typeof (define) === 'function' && define.amd) { // AMD 17 | define("detect-zoom", function () { 18 | return factory(ns, root); 19 | }); 20 | } else { 21 | root[ns] = factory(ns, root); 22 | } 23 | 24 | }(window, 'detectZoom', function () { 25 | 26 | /** 27 | * Use devicePixelRatio if supported by the browser 28 | * @return {Number} 29 | * @private 30 | */ 31 | var devicePixelRatio = function () { 32 | return window.devicePixelRatio || 1; 33 | }; 34 | 35 | /** 36 | * Fallback function to set default values 37 | * @return {Object} 38 | * @private 39 | */ 40 | var fallback = function () { 41 | return { 42 | zoom: 1, 43 | devicePxPerCssPx: 1 44 | }; 45 | }; 46 | /** 47 | * IE 8 and 9: no trick needed! 48 | * TODO: Test on IE10 and Windows 8 RT 49 | * @return {Object} 50 | * @private 51 | **/ 52 | var ie8 = function () { 53 | var zoom = Math.round((screen.deviceXDPI / screen.logicalXDPI) * 100) / 100; 54 | return { 55 | zoom: zoom, 56 | devicePxPerCssPx: zoom * devicePixelRatio() 57 | }; 58 | }; 59 | 60 | /** 61 | * For IE10 we need to change our technique again... 62 | * thanks https://github.com/stefanvanburen 63 | * @return {Object} 64 | * @private 65 | */ 66 | var ie10 = function () { 67 | var zoom = Math.round((document.documentElement.offsetHeight / window.innerHeight) * 100) / 100; 68 | return { 69 | zoom: zoom, 70 | devicePxPerCssPx: zoom * devicePixelRatio() 71 | }; 72 | }; 73 | 74 | /** 75 | * For chrome 76 | * 77 | */ 78 | var chrome = function() 79 | { 80 | var zoom = Math.round(((window.outerWidth) / window.innerWidth)*100) / 100; 81 | return { 82 | zoom: zoom, 83 | devicePxPerCssPx: zoom * devicePixelRatio() 84 | }; 85 | } 86 | 87 | /** 88 | * For safari (same as chrome) 89 | * 90 | */ 91 | var safari= function() 92 | { 93 | var zoom = Math.round(((document.documentElement.clientWidth) / window.innerWidth)*100) / 100; 94 | return { 95 | zoom: zoom, 96 | devicePxPerCssPx: zoom * devicePixelRatio() 97 | }; 98 | } 99 | 100 | 101 | /** 102 | * Mobile WebKit 103 | * the trick: window.innerWIdth is in CSS pixels, while 104 | * screen.width and screen.height are in system pixels. 105 | * And there are no scrollbars to mess up the measurement. 106 | * @return {Object} 107 | * @private 108 | */ 109 | var webkitMobile = function () { 110 | var deviceWidth = (Math.abs(window.orientation) == 90) ? screen.height : screen.width; 111 | var zoom = deviceWidth / window.innerWidth; 112 | return { 113 | zoom: zoom, 114 | devicePxPerCssPx: zoom * devicePixelRatio() 115 | }; 116 | }; 117 | 118 | /** 119 | * Desktop Webkit 120 | * the trick: an element's clientHeight is in CSS pixels, while you can 121 | * set its line-height in system pixels using font-size and 122 | * -webkit-text-size-adjust:none. 123 | * device-pixel-ratio: http://www.webkit.org/blog/55/high-dpi-web-sites/ 124 | * 125 | * Previous trick (used before http://trac.webkit.org/changeset/100847): 126 | * documentElement.scrollWidth is in CSS pixels, while 127 | * document.width was in system pixels. Note that this is the 128 | * layout width of the document, which is slightly different from viewport 129 | * because document width does not include scrollbars and might be wider 130 | * due to big elements. 131 | * @return {Object} 132 | * @private 133 | */ 134 | var webkit = function () { 135 | var important = function (str) { 136 | return str.replace(/;/g, " !important;"); 137 | }; 138 | 139 | var div = document.createElement('div'); 140 | div.innerHTML = "1
2
3
4
5
6
7
8
9
0"; 141 | 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;')); 142 | 143 | // The container exists so that the div will be laid out in its own flow 144 | // while not impacting the layout, viewport size, or display of the 145 | // webpage as a whole. 146 | // Add !important and relevant CSS rule resets 147 | // so that other rules cannot affect the results. 148 | var container = document.createElement('div'); 149 | container.setAttribute('style', important('width:0; height:0; overflow:hidden; visibility:hidden; position: absolute;')); 150 | container.appendChild(div); 151 | 152 | document.body.appendChild(container); 153 | var zoom = 1000 / div.clientHeight; 154 | zoom = Math.round(zoom * 100) / 100; 155 | document.body.removeChild(container); 156 | 157 | return{ 158 | zoom: zoom, 159 | devicePxPerCssPx: zoom * devicePixelRatio() 160 | }; 161 | }; 162 | 163 | /** 164 | * no real trick; device-pixel-ratio is the ratio of device dpi / css dpi. 165 | * (Note that this is a different interpretation than Webkit's device 166 | * pixel ratio, which is the ratio device dpi / system dpi). 167 | * 168 | * Also, for Mozilla, there is no difference between the zoom factor and the device ratio. 169 | * 170 | * @return {Object} 171 | * @private 172 | */ 173 | var firefox4 = function () { 174 | var zoom = mediaQueryBinarySearch('min--moz-device-pixel-ratio', '', 0, 10, 20, 0.0001); 175 | zoom = Math.round(zoom * 100) / 100; 176 | return { 177 | zoom: zoom, 178 | devicePxPerCssPx: zoom 179 | }; 180 | }; 181 | 182 | /** 183 | * Firefox 18.x 184 | * Mozilla added support for devicePixelRatio to Firefox 18, 185 | * but it is affected by the zoom level, so, like in older 186 | * Firefox we can't tell if we are in zoom mode or in a device 187 | * with a different pixel ratio 188 | * @return {Object} 189 | * @private 190 | */ 191 | var firefox18 = function () { 192 | return { 193 | zoom: firefox4().zoom, 194 | devicePxPerCssPx: devicePixelRatio() 195 | }; 196 | }; 197 | 198 | /** 199 | * works starting Opera 11.11 200 | * the trick: outerWidth is the viewport width including scrollbars in 201 | * system px, while innerWidth is the viewport width including scrollbars 202 | * in CSS px 203 | * @return {Object} 204 | * @private 205 | */ 206 | var opera11 = function () { 207 | var zoom = window.top.outerWidth / window.top.innerWidth; 208 | zoom = Math.round(zoom * 100) / 100; 209 | return { 210 | zoom: zoom, 211 | devicePxPerCssPx: zoom * devicePixelRatio() 212 | }; 213 | }; 214 | 215 | /** 216 | * Use a binary search through media queries to find zoom level in Firefox 217 | * @param property 218 | * @param unit 219 | * @param a 220 | * @param b 221 | * @param maxIter 222 | * @param epsilon 223 | * @return {Number} 224 | */ 225 | var mediaQueryBinarySearch = function (property, unit, a, b, maxIter, epsilon) { 226 | var matchMedia; 227 | var head, style, div; 228 | if (window.matchMedia) { 229 | matchMedia = window.matchMedia; 230 | } else { 231 | head = document.getElementsByTagName('head')[0]; 232 | style = document.createElement('style'); 233 | head.appendChild(style); 234 | 235 | div = document.createElement('div'); 236 | div.className = 'mediaQueryBinarySearch'; 237 | div.style.display = 'none'; 238 | document.body.appendChild(div); 239 | 240 | matchMedia = function (query) { 241 | style.sheet.insertRule('@media ' + query + '{.mediaQueryBinarySearch ' + '{text-decoration: underline} }', 0); 242 | var matched = getComputedStyle(div, null).textDecoration == 'underline'; 243 | style.sheet.deleteRule(0); 244 | return {matches: matched}; 245 | }; 246 | } 247 | var ratio = binarySearch(a, b, maxIter); 248 | if (div) { 249 | head.removeChild(style); 250 | document.body.removeChild(div); 251 | } 252 | return ratio; 253 | 254 | function binarySearch(a, b, maxIter) { 255 | var mid = (a + b) / 2; 256 | if (maxIter <= 0 || b - a < epsilon) { 257 | return mid; 258 | } 259 | var query = "(" + property + ":" + mid + unit + ")"; 260 | if (matchMedia(query).matches) { 261 | return binarySearch(mid, b, maxIter - 1); 262 | } else { 263 | return binarySearch(a, mid, maxIter - 1); 264 | } 265 | } 266 | }; 267 | 268 | /** 269 | * Generate detection function 270 | * @private 271 | */ 272 | var detectFunction = (function () { 273 | var func = fallback; 274 | //IE8+ 275 | if (!isNaN(screen.logicalXDPI) && !isNaN(screen.systemXDPI)) { 276 | func = ie8; 277 | } 278 | // IE10+ / Touch 279 | else if (window.navigator.msMaxTouchPoints) { 280 | func = ie10; 281 | } 282 | //chrome 283 | else if(!!window.chrome && !(!!window.opera || navigator.userAgent.indexOf(' Opera') >= 0)){ 284 | func = chrome; 285 | } 286 | //safari 287 | else if(Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0){ 288 | func = safari; 289 | } 290 | //Mobile Webkit 291 | else if ('orientation' in window && 'webkitRequestAnimationFrame' in window) { 292 | func = webkitMobile; 293 | } 294 | //WebKit 295 | else if ('webkitRequestAnimationFrame' in window) { 296 | func = webkit; 297 | } 298 | //Opera 299 | else if (navigator.userAgent.indexOf('Opera') >= 0) { 300 | func = opera11; 301 | } 302 | //Last one is Firefox 303 | //FF 18.x 304 | else if (window.devicePixelRatio) { 305 | func = firefox18; 306 | } 307 | //FF 4.0 - 17.x 308 | else if (firefox4().zoom > 0.001) { 309 | func = firefox4; 310 | } 311 | 312 | return func; 313 | }()); 314 | 315 | 316 | return ({ 317 | 318 | /** 319 | * Ratios.zoom shorthand 320 | * @return {Number} Zoom level 321 | */ 322 | zoom: function () { 323 | return detectFunction().zoom; 324 | }, 325 | 326 | /** 327 | * Ratios.devicePxPerCssPx shorthand 328 | * @return {Number} devicePxPerCssPx level 329 | */ 330 | device: function () { 331 | return detectFunction().devicePxPerCssPx; 332 | } 333 | }); 334 | })); 335 | -------------------------------------------------------------------------------- /detect-zoom.min.js: -------------------------------------------------------------------------------- 1 | (function(root,ns,factory){"use strict";"undefined"!=typeof module&&module.exports?module.exports=factory(ns,root):"function"==typeof define&&define.amd?define("detect-zoom",function(){return factory(ns,root)}):root[ns]=factory(ns,root)})(window,"detectZoom",function(){var devicePixelRatio=function(){return window.devicePixelRatio||1},fallback=function(){return{zoom:1,devicePxPerCssPx:1}},ie8=function(){var zoom=Math.round(100*(screen.deviceXDPI/screen.logicalXDPI))/100;return{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},ie10=function(){var zoom=Math.round(100*(document.documentElement.offsetHeight/window.innerHeight))/100;return{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},webkitMobile=function(){var deviceWidth=90==Math.abs(window.orientation)?screen.height:screen.width,zoom=deviceWidth/window.innerWidth;return{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},webkit=function(){var important=function(str){return str.replace(/;/g," !important;")},div=document.createElement("div");div.innerHTML="1
2
3
4
5
6
7
8
9
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;"));var container=document.createElement("div");container.setAttribute("style",important("width:0; height:0; overflow:hidden; visibility:hidden; position: absolute;")),container.appendChild(div),document.body.appendChild(container);var zoom=1e3/div.clientHeight;return zoom=Math.round(100*zoom)/100,document.body.removeChild(container),{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},firefox4=function(){var zoom=mediaQueryBinarySearch("min--moz-device-pixel-ratio","",0,10,20,1e-4);return zoom=Math.round(100*zoom)/100,{zoom:zoom,devicePxPerCssPx:zoom}},firefox18=function(){return{zoom:firefox4().zoom,devicePxPerCssPx:devicePixelRatio()}},opera11=function(){var zoom=window.top.outerWidth/window.top.innerWidth;return zoom=Math.round(100*zoom)/100,{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},mediaQueryBinarySearch=function(property,unit,a,b,maxIter,epsilon){function binarySearch(a,b,maxIter){var mid=(a+b)/2;if(0>=maxIter||epsilon>b-a)return mid;var query="("+property+":"+mid+unit+")";return matchMedia(query).matches?binarySearch(mid,b,maxIter-1):binarySearch(a,mid,maxIter-1)}var matchMedia,head,style,div;window.matchMedia?matchMedia=window.matchMedia:(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);var matched="underline"==getComputedStyle(div,null).textDecoration;return style.sheet.deleteRule(0),{matches:matched}});var ratio=binarySearch(a,b,maxIter);return div&&(head.removeChild(style),document.body.removeChild(div)),ratio},detectFunction=function(){var func=fallback;return isNaN(screen.logicalXDPI)||isNaN(screen.systemXDPI)?window.navigator.msMaxTouchPoints?func=ie10:"orientation"in window&&"string"==typeof document.body.style.webkitMarquee?func=webkitMobile:"string"==typeof document.body.style.webkitMarquee?func=webkit:navigator.userAgent.indexOf("Opera")>=0?func=opera11:window.devicePixelRatio?func=firefox18:firefox4().zoom>.001&&(func=firefox4):func=ie8,func}();return{zoom:function(){return detectFunction().zoom},device:function(){return detectFunction().devicePxPerCssPx}}}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "detect-zoom", 3 | "version": "1.0.4", 4 | "description": "Cross Browser Zoom and Pixel Ratio Detector", 5 | "main": "detect-zoom.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/tombigel/detect-zoom.git" 12 | }, 13 | "keywords": [ 14 | "browser", 15 | "zoom", 16 | "compatibility", 17 | "pixel", 18 | "ratio", 19 | "retina" 20 | ], 21 | "author": "Yonathan Randolph", 22 | "contributors": [ 23 | "Tom Bigelajzen " 24 | ], 25 | "license": "MIT", 26 | "readmeFilename": "README.md", 27 | "gitHead": "6eaf3107a6913a4f7b93665ba6d5bc16cdf0f3ab", 28 | "devDependencies": { 29 | "uglify-js": "~3.6.6" 30 | } 31 | } 32 | --------------------------------------------------------------------------------