├── .gitignore ├── CHANGELOG ├── MIT-LICENSE.txt ├── README.md ├── demo ├── common │ ├── debug.js │ └── style.css ├── default.html ├── devicepixel.html ├── disabled.html ├── map.html ├── nopagescroll.html ├── orientationlock.html ├── resize.html └── swipey.html ├── jasyproject.json └── src ├── viewporter.js └── viewporter.native.js /.gitignore: -------------------------------------------------------------------------------- 1 | jasycache* 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v2.1 2 | 3 | * added new option "forceDetection" to disable internal device profiles for custom resolutions 4 | * new devicepixel.html demo that brings back the native pixel mapping functionality back to Viewporter 5 | 6 | v2.0 7 | 8 | * completely refactored viewport detection 9 | * removed: maxDensity setting (use v1 if needed), now defaulting to device-width 10 | * removed: all debug constants except for viewporter.ACTIVE 11 | * added: Viewporter now needs a wrapper div around the body with the id 'viewporter' 12 | * added: Sites running Viewport now need the following meta viewport tag: -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Zynga Inc., http://zynga.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Zynga Viewporter v2.1 2 | ================ 3 | 4 | Viewporter is a open-source JavaScript project by Zynga to ease mobile viewport management. It specifically simplifies the part of setting up the right screen dimensions and removes the pain from handling the *viewport* meta tag manually. 5 | 6 | What does it do? 7 | ---------------- 8 | 9 | When put into the header of a page and when running a mobile device, Viewporter will first try to scroll away any URL or debug bars to maximize the visible window, and then substracts the remaining chrome/UI height from the window, effectively removing ugly scrollbars along the way. It will also track orientationchange, thus, you will always have a maximized viewing experience. 10 | 11 | How to use? 12 | ----------- 13 | 14 | In v1, all you had to to was to put Viewporter into the head of the page. There's just a little bit more to do in v2, but it isn't painful: 15 | 16 | # Add the following meta viewport to the of your page: 17 | 18 | # Wrap your element with the viewporter wrapper div: 19 | 20 |
21 | ... 22 |
23 | 24 | 25 | That's it, really! Feel free to have a look at the demo pages if something doesn't work as expected. 26 | 27 | What's wrong with doing it manually? 28 | ------------------------------------ 29 | 30 | You could of course try to set the viewport meta tag yourselves, as suggested in [various](https://developer.mozilla.org/en/mobile/viewport_meta_tag) [places](http://dev.opera.com/articles/view/an-introduction-to-meta-viewport-and-viewport/), usually something like *<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">*. You will quickly recognize two apparent issues: 31 | 32 | * proportional device-height doesn't subtract the chrome height, so the window is always larger than the viewport when set, causing scrollbars even on empty pages 33 | * rotating the device will cause the page to zoom (as device-width isn't inverted on rotation) 34 | * even with a manually fixed viewport, there's a stupid gap at the bottom of the page (when using absolutely positioned elements) 35 | 36 | Advantages of using Viewporter 37 | -------------------- 38 | 39 | So what's in it for you? There's a couple of automatic advantages for you when the Viewporter is running. Here's a list: 40 | 41 | * Maximized viewport (scrolling away unneeded UI) 42 | * Easy layouting 43 | 44 | Easy layouting? 45 | --------------- 46 | 47 | Yep. Take a *<div>*, position it absolutely, set its width and height to "50%", left and bottom to 0 and the background to any color. With Viewporter enabled, it will be placed at the bottom left corner of the window, and stretch to the middle of the window. Sounds obvious right? It isn't really, when you want a maximized window. 48 | 49 | API 50 | --- 51 | 52 | Viewporter is almost zero configuration. There's only one constant to check if Viewporter is in fact running, a convienience method to detect landscape orientation and a smart ready callback function. In addition, there's a couple of events you will likely want to use. 53 | 54 | ### Options 55 | 56 | * viewporter.forceDetection (Boolean) - defaults to false, enabling it will cause the Viewporter not to use its profiles for devices (see devicepixel demo) 57 | 58 | * viewporter.preventPageScroll (Boolean) - defaults to false, enabling it will prevent scroll events on the body element. Use this option to cancel iOS overscroll effect, which causes the view to bounce back when scrolling exceeds the bounds. Additionally it will scroll back the pane if the user clicks the page after selecting the address bar on iOS. 59 | 60 | ### Constants 61 | 62 | * viewporter.ACTIVE - _true_ if the Viewporter is enabled and running (smartphones!), false if not (Desktop, non-touch device) 63 | * viewporter.READY - _true_ when the viewportready function has already been fired. Useful if you're lazy loading initializing code 64 | 65 | ### Methods 66 | 67 | * viewporter.isLandscape() - returns wether the device is rotated to landscape or not 68 | * viewporter.ready() - accepts a callback and fires it when the viewporter has been successfully executed 69 | * viewporter.refresh() - refreshes the viewport. This is eg. useful when the browser displays an inline confirmations such as the geolocation alert on Android. **Hint**: Listen for `resize` events and then call this method. 70 | 71 | ### Events 72 | 73 | All events fire as native events on the window object. 74 | 75 | * viewportready - fires as soon as the Viewporter has been executed for the first time 76 | * viewportchange - fires when the viewport changes, i.e. the device is rotated, and after Viewporter has been executed again -------------------------------------------------------------------------------- /demo/common/debug.js: -------------------------------------------------------------------------------- 1 | var log = function() { 2 | $("#viewporter").append("

"+Array.prototype.slice.call(arguments).join(' ')+"

"); 3 | }; 4 | 5 | var debug = function() { 6 | 7 | log('landscape:', viewporter.isLandscape(), '('+window.orientation+')'); 8 | 9 | $('#viewporter-debug-width').text(window.innerWidth); 10 | $('#viewporter-debug-height').text(window.innerHeight); 11 | $('#viewporter-debug-useragent').text(navigator.userAgent); 12 | 13 | }; 14 | 15 | $(window).bind(viewporter.ACTIVE ? 'viewportready viewportchange' : 'load', function() { 16 | debug(); 17 | }); -------------------------------------------------------------------------------- /demo/common/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: Arial; 5 | font-size: 12px; 6 | color: white; 7 | background: green; 8 | -webkit-text-size-adjust: none; /* important, or your text resizes randomly on rotation! */ 9 | } 10 | 11 | p { 12 | margin: 0; 13 | padding: 1em; 14 | padding-bottom: 0; 15 | } 16 | 17 | #checker-bl, #checker-tr { 18 | position: absolute; 19 | width: 50%; 20 | height: 50%; 21 | } 22 | 23 | #checker-bl { 24 | position: absolute; 25 | background: blue; 26 | bottom: 0; 27 | left: 0; 28 | } 29 | 30 | #checker-tr { 31 | position: absolute; 32 | background: red; 33 | top: 0; 34 | right: 0; 35 | } 36 | 37 | #orientationlock { 38 | position: absolute; 39 | z-index: 1000; 40 | top: 0; 41 | left: 0; 42 | bottom: 0; 43 | right: 0; 44 | background: #333; 45 | color: #fff; 46 | text-shadow: 1px 1px 0px #000; 47 | font-size: 2em; 48 | text-align: center; 49 | display: none; 50 | } 51 | 52 | #orientationlock p { 53 | position: absolute; 54 | top: 50%; 55 | left: 0; 56 | width: 100%; 57 | margin-top: -2em; 58 | padding: 0; 59 | } 60 | 61 | canvas { 62 | background: #eee; 63 | position: absolute; 64 | top: 0; 65 | left: 0; 66 | } -------------------------------------------------------------------------------- /demo/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Viewporter enabled 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 |

window width:

29 |

window height:

30 |
31 |
32 |

user agent:

33 |
34 | 35 |
36 | 37 | -------------------------------------------------------------------------------- /demo/devicepixel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Viewporter enabled (Mapped to device pixels) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 |
29 |

window width:

30 |

window height:

31 |
32 |
33 |

user agent:

34 |
35 | 36 |
37 | 38 | -------------------------------------------------------------------------------- /demo/disabled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Viewporter disabled 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |

window width:

28 |

window height:

29 |
30 |
31 |

user agent:

32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /demo/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Google Maps Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 | 37 | 38 | 39 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /demo/nopagescroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | No Page Scroll 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |

24 |

25 | 26 |
27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demo/orientationlock.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Viewporter enabled 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 |
33 | 34 |

Please rotate your device to use this demo application.

35 | 36 |
37 |

window width:

38 |

window height:

39 |
40 |
41 |

user agent:

42 |
43 | 44 |
45 | 46 | -------------------------------------------------------------------------------- /demo/resize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Resize 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 |

25 |
26 |
27 |

28 |
29 | 30 |
31 | 32 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /demo/swipey.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swipey (Mapped to device pixels) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 199 | 200 | 201 | 202 |
203 | 204 |
205 | 206 | -------------------------------------------------------------------------------- /jasyproject.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "viewporter", 3 | "package" : "" 4 | } 5 | -------------------------------------------------------------------------------- /src/viewporter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Viewporter v2.0 3 | * http://github.com/zynga/viewporter 4 | * 5 | * Copyright 2011, Zynga Inc. 6 | * Licensed under the MIT License. 7 | * https://raw.github.com/zynga/viewporter/master/MIT-LICENSE.txt 8 | */ 9 | var viewporter; 10 | (function() { 11 | 12 | var _viewporter; 13 | 14 | // initialize viewporter object 15 | viewporter = { 16 | 17 | // options 18 | forceDetection: false, 19 | 20 | disableLegacyAndroid: true, 21 | 22 | // constants 23 | ACTIVE: (function() { 24 | 25 | // it's best not do to anything to very weak devices running Android 2.x 26 | if(viewporter.disableLegacyAndroid && (/android 2/i).test(navigator.userAgent)) { 27 | return false; 28 | } 29 | 30 | // iPad's don't allow you to scroll away the UI of the browser 31 | if((/ipad/i).test(navigator.userAgent)) { 32 | return false; 33 | } 34 | 35 | // WebOS has no touch events, but definitely the need for viewport normalization 36 | if((/webos/i).test(navigator.userAgent)) { 37 | return true; 38 | } 39 | 40 | // touch enabled devices 41 | if('ontouchstart' in window) { 42 | return true; 43 | } 44 | 45 | return false; 46 | 47 | }), 48 | 49 | READY: false, 50 | 51 | // methods 52 | isLandscape: function() { 53 | return window.orientation === 90 || window.orientation === -90; 54 | }, 55 | 56 | ready: function(callback) { 57 | window.addEventListener('viewportready', callback, false); 58 | }, 59 | 60 | change: function(callback) { 61 | window.addEventListener('viewportchange', callback, false); 62 | }, 63 | 64 | refresh: function(){ 65 | if (_viewporter) { 66 | _viewporter.prepareVisualViewport(); 67 | } 68 | }, 69 | 70 | preventPageScroll: function() { 71 | 72 | // prevent page scroll if `preventPageScroll` option was set to `true` 73 | document.body.addEventListener('touchmove', function(event) { 74 | event.preventDefault(); 75 | }, false); 76 | 77 | // reset page scroll if `preventPageScroll` option was set to `true` 78 | // this is used after showing the address bar on iOS 79 | document.body.addEventListener("touchstart", function() { 80 | _viewporter.prepareVisualViewport(); 81 | }, false); 82 | 83 | } 84 | 85 | }; 86 | 87 | // execute the ACTIVE flag 88 | viewporter.ACTIVE = viewporter.ACTIVE(); 89 | 90 | // if we are on Desktop, no need to go further 91 | if (!viewporter.ACTIVE) { 92 | return; 93 | } 94 | 95 | // create private constructor with prototype..just looks cooler 96 | var _Viewporter = function() { 97 | 98 | var that = this; 99 | 100 | // Scroll away the header, but not in Chrome 101 | this.IS_ANDROID = /Android/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); 102 | 103 | var _onReady = function() { 104 | 105 | // scroll the shit away and fix the viewport! 106 | that.prepareVisualViewport(); 107 | 108 | // listen for orientation change 109 | var cachedOrientation = window.orientation; 110 | window.addEventListener('orientationchange', function() { 111 | if(window.orientation !== cachedOrientation) { 112 | that.prepareVisualViewport(); 113 | cachedOrientation = window.orientation; 114 | } 115 | }, false); 116 | 117 | }; 118 | 119 | 120 | // listen for document ready if not already loaded 121 | // then try to prepare the visual viewport and start firing custom events 122 | if (document.readyState === 'loading') { 123 | document.addEventListener('DOMContentLoaded', function() { 124 | _onReady(); 125 | }, false); 126 | } else { 127 | _onReady(); 128 | } 129 | 130 | 131 | }; 132 | 133 | _Viewporter.prototype = { 134 | 135 | getProfile: function() { 136 | 137 | if(viewporter.forceDetection) { 138 | return null; 139 | } 140 | 141 | for(var searchTerm in viewporter.profiles) { 142 | if(new RegExp(searchTerm).test(navigator.userAgent)) { 143 | return viewporter.profiles[searchTerm]; 144 | } 145 | } 146 | return null; 147 | }, 148 | 149 | postProcess: function() { 150 | 151 | // let everyone know we're finally ready 152 | viewporter.READY = true; 153 | 154 | this.triggerWindowEvent(!this._firstUpdateExecuted ? 'viewportready' : 'viewportchange'); 155 | this._firstUpdateExecuted = true; 156 | 157 | }, 158 | 159 | prepareVisualViewport: function() { 160 | 161 | var that = this; 162 | 163 | // if we're running in webapp mode (iOS), there's nothing to scroll away 164 | if(navigator.standalone) { 165 | return this.postProcess(); 166 | } 167 | 168 | // maximize the document element's height to be able to scroll away the url bar 169 | document.documentElement.style.minHeight = '5000px'; 170 | 171 | var startHeight = window.innerHeight; 172 | var deviceProfile = this.getProfile(); 173 | var orientation = viewporter.isLandscape() ? 'landscape' : 'portrait'; 174 | 175 | // try scrolling immediately 176 | window.scrollTo(0, that.IS_ANDROID ? 1 : 0); // Android needs to scroll by at least 1px 177 | 178 | // start the checker loop 179 | var iterations = 40; 180 | var check = window.setInterval(function() { 181 | 182 | // retry scrolling 183 | window.scrollTo(0, that.IS_ANDROID ? 1 : 0); // Android needs to scroll by at least 1px 184 | 185 | function androidProfileCheck() { 186 | return deviceProfile ? window.innerHeight === deviceProfile[orientation] : false; 187 | } 188 | function iosInnerHeightCheck() { 189 | return window.innerHeight > startHeight; 190 | } 191 | 192 | iterations--; 193 | 194 | // check iterations first to make sure we never get stuck 195 | if ( (that.IS_ANDROID ? androidProfileCheck() : iosInnerHeightCheck()) || iterations < 0) { 196 | 197 | // set minimum height of content to new window height 198 | document.documentElement.style.minHeight = window.innerHeight + 'px'; 199 | 200 | // set the right height for the body wrapper to allow bottom positioned elements 201 | document.getElementById('viewporter').style.position = 'relative'; 202 | document.getElementById('viewporter').style.height = window.innerHeight + 'px'; 203 | 204 | clearInterval(check); 205 | 206 | // fire events, get ready 207 | that.postProcess(); 208 | 209 | } 210 | 211 | }, 10); 212 | 213 | }, 214 | 215 | triggerWindowEvent: function(name) { 216 | var event = document.createEvent("Event"); 217 | event.initEvent(name, false, false); 218 | window.dispatchEvent(event); 219 | } 220 | 221 | }; 222 | 223 | // initialize 224 | _viewporter = new _Viewporter(); 225 | 226 | })(); 227 | 228 | viewporter.profiles = { 229 | 230 | // Motorola Xoom 231 | 'MZ601': { 232 | portrait: 696, 233 | landscape: 1176 234 | }, 235 | 236 | // Samsung Galaxy S, S2 and Nexus S 237 | 'GT-I9000|GT-I9100|Nexus S': { 238 | portrait: 508, 239 | landscape: 295 240 | }, 241 | 242 | // Samsung Galaxy Pad 243 | 'GT-P1000': { 244 | portrait: 657, 245 | landscape: 400 246 | }, 247 | 248 | // HTC Desire & HTC Desire HD 249 | 'Desire_A8181|DesireHD_A9191': { 250 | portrait: 533, 251 | landscape: 320 252 | } 253 | 254 | }; -------------------------------------------------------------------------------- /src/viewporter.native.js: -------------------------------------------------------------------------------- 1 | viewporter.forceDetection = true; 2 | 3 | (function() { 4 | 5 | var scale = 1; 6 | 7 | if(/iPhone|iPad/.test(navigator.userAgent) && window.devicePixelRatio > 1) { 8 | scale /= window.devicePixelRatio; 9 | } 10 | 11 | document.write(' -1 ? 'target-densitydpi=device-dpi,' : '') + 'initial-scale=' + scale + ',maximum-scale=' + scale + '" />'); 12 | 13 | })(); --------------------------------------------------------------------------------