/viewport-units-buggyfill.js
  1. /*!
  2.  * viewport-units-buggyfill v0.6.0
  3.  * @web: https://github.com/rodneyrehm/viewport-units-buggyfill/
  4.  * @author: Rodney Rehm - http://rodneyrehm.de/en/
  5.  */
  6.  
  7. (function (root, factory) {
  8.   'use strict';
  9.   if (typeof define === 'function' && define.amd) {
  10.     // AMD. Register as an anonymous module.
  11.     define([], factory);
  12.   } else if (typeof exports === 'object') {
  13.     // Node. Does not work with strict CommonJS, but
  14.     // only CommonJS-like enviroments that support module.exports,
  15.     // like Node.
  16.     module.exports = factory();
  17.   } else {
  18.     // Browser globals (root is window)
  19.     root.viewportUnitsBuggyfill = factory();
  20.   }
  21. }(this, function () {
  22.   'use strict';
  23.   /*global document, window, navigator, location, XMLHttpRequest, XDomainRequest, CustomEvent*/
  24.  
  25.   var initialized = false;
  26.   var options;
  27.   var userAgent = window.navigator.userAgent;
  28.   var viewportUnitExpression = /([+-]?[0-9.]+)(vh|vw|vmin|vmax)/g;
  29.   var forEach = [].forEach;
  30.   var dimensions;
  31.   var declarations;
  32.   var styleNode;
  33.   var isBuggyIE = /MSIE [0-9]\./i.test(userAgent);
  34.   var isOldIE = /MSIE [0-8]\./i.test(userAgent);
  35.   var isOperaMini = userAgent.indexOf('Opera Mini') > -1;
  36.  
  37.   var isMobileSafari = /(iPhone|iPod|iPad).+AppleWebKit/i.test(userAgent) && (function() {
  38.     // Regexp for iOS-version tested against the following userAgent strings:
  39.     // Example WebView UserAgents:
  40.     // * iOS Chrome on iOS8: "Mozilla/5.0 (iPad; CPU OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/39.0.2171.50 Mobile/12B410 Safari/600.1.4"
  41.     // * iOS Facebook on iOS7: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D201 [FBAN/FBIOS;FBAV/12.1.0.24.20; FBBV/3214247; FBDV/iPhone6,1;FBMD/iPhone; FBSN/iPhone OS;FBSV/7.1.1; FBSS/2; FBCR/AT&T;FBID/phone;FBLC/en_US;FBOP/5]"
  42.     // Example Safari UserAgents:
  43.     // * Safari iOS8: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4"
  44.     // * Safari iOS7: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A4449d Safari/9537.53"
  45.     var iOSversion = userAgent.match(/OS (\d)/);
  46.     // viewport units work fine in mobile Safari and webView on iOS 8+
  47.     return iOSversion && iOSversion.length>1 && parseInt(iOSversion[1]) < 10;
  48.   })();
  49.  
  50.   var isBadStockAndroid = (function() {
  51.     // Android stock browser test derived from
  52.     // http://stackoverflow.com/questions/24926221/distinguish-android-chrome-from-stock-browser-stock-browsers-user-agent-contai
  53.     var isAndroid = userAgent.indexOf(' Android ') > -1;
  54.     if (!isAndroid) {
  55.       return false;
  56.     }
  57.  
  58.     var isStockAndroid = userAgent.indexOf('Version/') > -1;
  59.     if (!isStockAndroid) {
  60.       return false;
  61.     }
  62.  
  63.     var versionNumber = parseFloat((userAgent.match('Android ([0-9.]+)') || [])[1]);
  64.     // anything below 4.4 uses WebKit without *any* viewport support,
  65.     // 4.4 has issues with viewport units within calc()
  66.     return versionNumber <= 4.4;
  67.   })();
  68.  
  69.   // added check for IE10, IE11 and Edge < 20, since it *still* doesn't understand vmax
  70.   // http://caniuse.com/#feat=viewport-units
  71.   if (!isBuggyIE) {
  72.     isBuggyIE = !!navigator.userAgent.match(/MSIE 10\.|Trident.*rv[ :]*1[01]\.| Edge\/1\d\./);
  73.   }
  74.  
  75.   // Polyfill for creating CustomEvents on IE9/10/11
  76.   // from https://github.com/krambuhl/custom-event-polyfill
  77.   try {
  78.     new CustomEvent('test');
  79.   } catch(e) {
  80.     var CustomEvent = function(event, params) {
  81.       var evt;
  82.       params = params || {
  83.         bubbles: false,
  84.         cancelable: false,
  85.         detail: undefined
  86.       };
  87.  
  88.       evt = document.createEvent('CustomEvent');
  89.       evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
  90.       return evt;
  91.     };
  92.     CustomEvent.prototype = window.Event.prototype;
  93.     window.CustomEvent = CustomEvent; // expose definition to window
  94.   }
  95.  
  96.   function debounce(func, wait) {
  97.     var timeout;
  98.     return function() {
  99.       var context = this;
  100.       var args = arguments;
  101.       var callback = function() {
  102.         func.apply(context, args);
  103.       };
  104.  
  105.       clearTimeout(timeout);
  106.       timeout = setTimeout(callback, wait);
  107.     };
  108.   }
  109.  
  110.   // from http://stackoverflow.com/questions/326069/how-to-identify-if-a-webpage-is-being-loaded-inside-an-iframe-or-directly-into-t
  111.   function inIframe() {
  112.     try {
  113.       return window.self !== window.top;
  114.     } catch (e) {
  115.       return true;
  116.     }
  117.   }
  118.  
  119.   function initialize(initOptions) {
  120.     if (initialized) {
  121.       return;
  122.     }
  123.  
  124.     if (initOptions === true) {
  125.       initOptions = {
  126.         force: true
  127.       };
  128.     }
  129.  
  130.     options = initOptions || {};
  131.     options.isMobileSafari = isMobileSafari;
  132.     options.isBadStockAndroid = isBadStockAndroid;
  133.  
  134.     if (options.ignoreVmax && !options.force && !isOldIE) {
  135.       // modern IE (10 and up) do not support vmin/vmax,
  136.       // but chances are this unit is not even used, so
  137.       // allow overwriting the "hacktivation"
  138.       // https://github.com/rodneyrehm/viewport-units-buggyfill/issues/56
  139.       isBuggyIE = false;
  140.     }
  141.  
  142.     if (isOldIE || (!options.force && !isMobileSafari && !isBuggyIE && !isBadStockAndroid && !isOperaMini && (!options.hacks || !options.hacks.required(options)))) {
  143.       // this buggyfill only applies to mobile safari, IE9-10 and the Stock Android Browser.
  144.       if (window.console && isOldIE) {
  145.         console.info('viewport-units-buggyfill requires a proper CSSOM and basic viewport unit support, which are not available in IE8 and below');
  146.       }
  147.  
  148.       return {
  149.         init: function () {}
  150.       };
  151.     }
  152.  
  153.     // fire a custom event that buggyfill was initialize
  154.     window.dispatchEvent(new CustomEvent('viewport-units-buggyfill-init'));
  155.  
  156.     options.hacks && options.hacks.initialize(options);
  157.  
  158.     initialized = true;
  159.     styleNode = document.createElement('style');
  160.     styleNode.id = 'patched-viewport';
  161.     document.head.appendChild(styleNode);
  162.  
  163.     // Issue #6: Cross Origin Stylesheets are not accessible through CSSOM,
  164.     // therefore download and inject them as <style> to circumvent SOP.
  165.     importCrossOriginLinks(function() {
  166.       var _refresh = debounce(refresh, options.refreshDebounceWait || 100);
  167.       // doing a full refresh rather than updateStyles because an orientationchange
  168.       // could activate different stylesheets
  169.       window.addEventListener('orientationchange', _refresh, true);
  170.       // orientationchange might have happened while in a different window
  171.       window.addEventListener('pageshow', _refresh, true);
  172.  
  173.       if (options.force || isBuggyIE || inIframe()) {
  174.         window.addEventListener('resize', _refresh, true);
  175.         options._listeningToResize = true;
  176.       }
  177.  
  178.       options.hacks && options.hacks.initializeEvents(options, refresh, _refresh);
  179.  
  180.       refresh();
  181.     });
  182.   }
  183.  
  184.   function updateStyles() {
  185.     styleNode.textContent = getReplacedViewportUnits();
  186.     // move to the end in case inline <style>s were added dynamically
  187.     styleNode.parentNode.appendChild(styleNode);
  188.     // fire a custom event that styles were updated
  189.     window.dispatchEvent(new CustomEvent('viewport-units-buggyfill-style'));
  190.   }
  191.  
  192.   function refresh() {
  193.     if (!initialized) {
  194.       return;
  195.     }
  196.  
  197.     findProperties();
  198.  
  199.     // iOS Safari will report window.innerWidth and .innerHeight as 0 unless a timeout is used here.
  200.     // TODO: figure out WHY innerWidth === 0
  201.     setTimeout(function() {
  202.       updateStyles();
  203.     }, 1);
  204.   }
  205.  
  206.   // http://stackoverflow.com/a/23613052
  207.   function processStylesheet(ss) {
  208.     // cssRules respects same-origin policy, as per
  209.     // https://code.google.com/p/chromium/issues/detail?id=49001#c10.
  210.     try {
  211.       if (!ss.cssRules) { return; }
  212.     } catch(e) {
  213.       if (e.name !== 'SecurityError') { throw e; }
  214.       return;
  215.     }
  216.     // ss.cssRules is available, so proceed with desired operations.
  217.     var rules = [];
  218.     for (var i = 0; i < ss.cssRules.length; i++) {
  219.       var rule = ss.cssRules[i];
  220.       rules.push(rule);
  221.     }
  222.     return rules;
  223.   }
  224.  
  225.   function findProperties() {
  226.     declarations = [];
  227.     forEach.call(document.styleSheets, function(sheet) {
  228.       var cssRules = processStylesheet(sheet);
  229.  
  230.       if (!cssRules || sheet.ownerNode.id === 'patched-viewport' || sheet.ownerNode.getAttribute('data-viewport-units-buggyfill') === 'ignore') {
  231.         // skip entire sheet because no rules are present, it's supposed to be ignored or it's the target-element of the buggyfill
  232.         return;
  233.       }
  234.  
  235.       if (sheet.media && sheet.media.mediaText && window.matchMedia && !window.matchMedia(sheet.media.mediaText).matches) {
  236.         // skip entire sheet because media attribute doesn't match
  237.         return;
  238.       }
  239.  
  240.       forEach.call(cssRules, findDeclarations);
  241.     });
  242.  
  243.     return declarations;
  244.   }
  245.  
  246.   function findDeclarations(rule) {
  247.     if (rule.type === 7) {
  248.       var value;
  249.  
  250.       // there may be a case where accessing cssText throws an error.
  251.       // I could not reproduce this issue, but the worst that can happen
  252.       // this way is an animation not running properly.
  253.       // not awesome, but probably better than a script error
  254.       // see https://github.com/rodneyrehm/viewport-units-buggyfill/issues/21
  255.       try {
  256.         value = rule.cssText;
  257.       } catch(e) {
  258.         return;
  259.       }
  260.  
  261.       viewportUnitExpression.lastIndex = 0;
  262.       if (viewportUnitExpression.test(value)) {
  263.         // KeyframesRule does not have a CSS-PropertyName
  264.         declarations.push([rule, null, value]);
  265.         options.hacks && options.hacks.findDeclarations(declarations, rule, null, value);
  266.       }
  267.  
  268.       return;
  269.     }
  270.  
  271.     if (!rule.style) {
  272.       if (!rule.cssRules) {
  273.         return;
  274.       }
  275.  
  276.       forEach.call(rule.cssRules, function(_rule) {
  277.         findDeclarations(_rule);
  278.       });
  279.  
  280.       return;
  281.     }
  282.  
  283.     forEach.call(rule.style, function(name) {
  284.       var value = rule.style.getPropertyValue(name);
  285.       // preserve those !important rules
  286.       if (rule.style.getPropertyPriority(name)) {
  287.         value += ' !important';
  288.       }
  289.  
  290.       viewportUnitExpression.lastIndex = 0;
  291.       if (viewportUnitExpression.test(value)) {
  292.         declarations.push([rule, name, value]);
  293.         options.hacks && options.hacks.findDeclarations(declarations, rule, name, value);
  294.       }
  295.     });
  296.   }
  297.  
  298.   function getReplacedViewportUnits() {
  299.     dimensions = getViewport();
  300.  
  301.     var css = [];
  302.     var buffer = [];
  303.     var open;
  304.     var close;
  305.  
  306.     declarations.forEach(function(item) {
  307.       var _item = overwriteDeclaration.apply(null, item);
  308.       var _open = _item.selector.length ? (_item.selector.join(' {\n') + ' {\n') : '';
  309.       var _close = new Array(_item.selector.length + 1).join('\n}');
  310.  
  311.       if (!_open || _open !== open) {
  312.         if (buffer.length) {
  313.           css.push(open + buffer.join('\n') + close);
  314.           buffer.length = 0;
  315.         }
  316.  
  317.         if (_open) {
  318.           open = _open;
  319.           close = _close;
  320.           buffer.push(_item.content);
  321.         } else {
  322.           css.push(_item.content);
  323.           open = null;
  324.           close = null;
  325.         }
  326.  
  327.         return;
  328.       }
  329.  
  330.       if (_open && !open) {
  331.         open = _open;
  332.         close = _close;
  333.       }
  334.  
  335.       buffer.push(_item.content);
  336.     });
  337.  
  338.     if (buffer.length) {
  339.       css.push(open + buffer.join('\n') + close);
  340.     }
  341.  
  342.     // Opera Mini messes up on the content hack (it replaces the DOM node's innerHTML with the value).
  343.     // This fixes it. We test for Opera Mini only since it is the most expensive CSS selector
  344.     // see https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors
  345.     if (isOperaMini) {
  346.       css.push('* { content: normal !important; }');
  347.     }
  348.  
  349.     return css.join('\n\n');
  350.   }
  351.  
  352.   function overwriteDeclaration(rule, name, value) {
  353.     var _value;
  354.     var _selectors = [];
  355.  
  356.     _value = value.replace(viewportUnitExpression, replaceValues);
  357.  
  358.     if (options.hacks) {
  359.       _value = options.hacks.overwriteDeclaration(rule, name, _value);
  360.     }
  361.  
  362.     if (name) {
  363.       // skipping KeyframesRule
  364.       _selectors.push(rule.selectorText);
  365.       _value = name + ': ' + _value + ';';
  366.     }
  367.  
  368.     var _rule = rule.parentRule;
  369.     while (_rule) {
  370.       _selectors.unshift('@media ' + _rule.media.mediaText);
  371.       _rule = _rule.parentRule;
  372.     }
  373.  
  374.     return {
  375.       selector: _selectors,
  376.       content: _value
  377.     };
  378.   }
  379.  
  380.   function replaceValues(match, number, unit) {
  381.     var _base = dimensions[unit];
  382.     var _number = parseFloat(number) / 100;
  383.     return (_number * _base) + 'px';
  384.   }
  385.  
  386.   function getViewport() {
  387.     var vh = window.innerHeight;
  388.     var vw = window.innerWidth;
  389.  
  390.     return {
  391.       vh: vh,
  392.       vw: vw,
  393.       vmax: Math.max(vw, vh),
  394.       vmin: Math.min(vw, vh)
  395.     };
  396.   }
  397.  
  398.   function importCrossOriginLinks(next) {
  399.     var _waiting = 0;
  400.     var decrease = function() {
  401.       _waiting--;
  402.       if (!_waiting) {
  403.         next();
  404.       }
  405.     };
  406.  
  407.     forEach.call(document.styleSheets, function(sheet) {
  408.       if (!sheet.href || origin(sheet.href) === origin(location.href) || sheet.ownerNode.getAttribute('data-viewport-units-buggyfill') === 'ignore') {
  409.         // skip <style> and <link> from same origin or explicitly declared to ignore
  410.         return;
  411.       }
  412.  
  413.       _waiting++;
  414.       convertLinkToStyle(sheet.ownerNode, decrease);
  415.     });
  416.  
  417.     if (!_waiting) {
  418.       next();
  419.     }
  420.   }
  421.  
  422.   function origin(url) {
  423.     return url.slice(0, url.indexOf('/', url.indexOf('://') + 3));
  424.   }
  425.  
  426.   function convertLinkToStyle(link, next) {
  427.     getCors(link.href, function() {
  428.       var style = document.createElement('style');
  429.       style.media = link.media;
  430.       style.setAttribute('data-href', link.href);
  431.       style.textContent = this.responseText;
  432.       link.parentNode.replaceChild(style, link);
  433.       next();
  434.     }, next);
  435.   }
  436.  
  437.   function getCors(url, success, error) {
  438.     var xhr = new XMLHttpRequest();
  439.     if ('withCredentials' in xhr) {
  440.       // XHR for Chrome/Firefox/Opera/Safari.
  441.       xhr.open('GET', url, true);
  442.     } else if (typeof XDomainRequest !== 'undefined') {
  443.       // XDomainRequest for IE.
  444.       xhr = new XDomainRequest();
  445.       xhr.open('GET', url);
  446.     } else {
  447.       throw new Error('cross-domain XHR not supported');
  448.     }
  449.  
  450.     xhr.onload = success;
  451.     xhr.onerror = error;
  452.     xhr.send();
  453.     return xhr;
  454.   }
  455.  
  456.   return {
  457.     version: '0.6.0',
  458.     findProperties: findProperties,
  459.     getCss: getReplacedViewportUnits,
  460.     init: initialize,
  461.     refresh: refresh
  462.   };
  463.  
  464. }));
Parsed in 0.078 seconds