respond.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. /* Respond.js: min/max-width media query polyfill. (c) Scott Jehl. MIT Lic. j.mp/respondjs */
  2. (function( w ){
  3. "use strict";
  4. //exposed namespace
  5. var respond = {};
  6. w.respond = respond;
  7. //define update even in native-mq-supporting browsers, to avoid errors
  8. respond.update = function(){};
  9. //define ajax obj
  10. var requestQueue = [],
  11. xmlHttp = (function() {
  12. var xmlhttpmethod = false;
  13. try {
  14. xmlhttpmethod = new w.XMLHttpRequest();
  15. }
  16. catch( e ){
  17. xmlhttpmethod = new w.ActiveXObject( "Microsoft.XMLHTTP" );
  18. }
  19. return function(){
  20. return xmlhttpmethod;
  21. };
  22. })(),
  23. //tweaked Ajax functions from Quirksmode
  24. ajax = function( url, callback ) {
  25. var req = xmlHttp();
  26. if (!req){
  27. return;
  28. }
  29. req.open( "GET", url, true );
  30. req.onreadystatechange = function () {
  31. if ( req.readyState !== 4 || req.status !== 200 && req.status !== 304 ){
  32. return;
  33. }
  34. callback( req.responseText );
  35. };
  36. if ( req.readyState === 4 ){
  37. return;
  38. }
  39. req.send( null );
  40. },
  41. isUnsupportedMediaQuery = function( query ) {
  42. return query.replace( respond.regex.minmaxwh, '' ).match( respond.regex.other );
  43. };
  44. //expose for testing
  45. respond.ajax = ajax;
  46. respond.queue = requestQueue;
  47. respond.unsupportedmq = isUnsupportedMediaQuery;
  48. respond.regex = {
  49. media: /@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,
  50. keyframes: /@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,
  51. comments: /\/\*[^*]*\*+([^/][^*]*\*+)*\//gi,
  52. urls: /(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,
  53. findStyles: /@media *([^\{]+)\{([\S\s]+?)$/,
  54. only: /(only\s+)?([a-zA-Z]+)\s?/,
  55. minw: /\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,
  56. maxw: /\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,
  57. minmaxwh: /\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi,
  58. other: /\([^\)]*\)/g
  59. };
  60. //expose media query support flag for external use
  61. respond.mediaQueriesSupported = w.matchMedia && w.matchMedia( "only all" ) !== null && w.matchMedia( "only all" ).matches;
  62. //if media queries are supported, exit here
  63. if( respond.mediaQueriesSupported ){
  64. return;
  65. }
  66. //define vars
  67. var doc = w.document,
  68. docElem = doc.documentElement,
  69. mediastyles = [],
  70. rules = [],
  71. appendedEls = [],
  72. parsedSheets = {},
  73. resizeThrottle = 30,
  74. head = doc.getElementsByTagName( "head" )[0] || docElem,
  75. base = doc.getElementsByTagName( "base" )[0],
  76. links = head.getElementsByTagName( "link" ),
  77. lastCall,
  78. resizeDefer,
  79. //cached container for 1em value, populated the first time it's needed
  80. eminpx,
  81. // returns the value of 1em in pixels
  82. getEmValue = function() {
  83. var ret,
  84. div = doc.createElement('div'),
  85. body = doc.body,
  86. originalHTMLFontSize = docElem.style.fontSize,
  87. originalBodyFontSize = body && body.style.fontSize,
  88. fakeUsed = false;
  89. div.style.cssText = "position:absolute;font-size:1em;width:1em";
  90. if( !body ){
  91. body = fakeUsed = doc.createElement( "body" );
  92. body.style.background = "none";
  93. }
  94. // 1em in a media query is the value of the default font size of the browser
  95. // reset docElem and body to ensure the correct value is returned
  96. docElem.style.fontSize = "100%";
  97. body.style.fontSize = "100%";
  98. body.appendChild( div );
  99. if( fakeUsed ){
  100. docElem.insertBefore( body, docElem.firstChild );
  101. }
  102. ret = div.offsetWidth;
  103. if( fakeUsed ){
  104. docElem.removeChild( body );
  105. }
  106. else {
  107. body.removeChild( div );
  108. }
  109. // restore the original values
  110. docElem.style.fontSize = originalHTMLFontSize;
  111. if( originalBodyFontSize ) {
  112. body.style.fontSize = originalBodyFontSize;
  113. }
  114. //also update eminpx before returning
  115. ret = eminpx = parseFloat(ret);
  116. return ret;
  117. },
  118. //enable/disable styles
  119. applyMedia = function( fromResize ){
  120. var name = "clientWidth",
  121. docElemProp = docElem[ name ],
  122. currWidth = doc.compatMode === "CSS1Compat" && docElemProp || doc.body[ name ] || docElemProp,
  123. styleBlocks = {},
  124. lastLink = links[ links.length-1 ],
  125. now = (new Date()).getTime();
  126. //throttle resize calls
  127. if( fromResize && lastCall && now - lastCall < resizeThrottle ){
  128. w.clearTimeout( resizeDefer );
  129. resizeDefer = w.setTimeout( applyMedia, resizeThrottle );
  130. return;
  131. }
  132. else {
  133. lastCall = now;
  134. }
  135. for( var i in mediastyles ){
  136. if( mediastyles.hasOwnProperty( i ) ){
  137. var thisstyle = mediastyles[ i ],
  138. min = thisstyle.minw,
  139. max = thisstyle.maxw,
  140. minnull = min === null,
  141. maxnull = max === null,
  142. em = "em";
  143. if( !!min ){
  144. min = parseFloat( min ) * ( min.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 );
  145. }
  146. if( !!max ){
  147. max = parseFloat( max ) * ( max.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 );
  148. }
  149. // if there's no media query at all (the () part), or min or max is not null, and if either is present, they're true
  150. if( !thisstyle.hasquery || ( !minnull || !maxnull ) && ( minnull || currWidth >= min ) && ( maxnull || currWidth <= max ) ){
  151. if( !styleBlocks[ thisstyle.media ] ){
  152. styleBlocks[ thisstyle.media ] = [];
  153. }
  154. styleBlocks[ thisstyle.media ].push( rules[ thisstyle.rules ] );
  155. }
  156. }
  157. }
  158. //remove any existing respond style element(s)
  159. for( var j in appendedEls ){
  160. if( appendedEls.hasOwnProperty( j ) ){
  161. if( appendedEls[ j ] && appendedEls[ j ].parentNode === head ){
  162. head.removeChild( appendedEls[ j ] );
  163. }
  164. }
  165. }
  166. appendedEls.length = 0;
  167. //inject active styles, grouped by media type
  168. for( var k in styleBlocks ){
  169. if( styleBlocks.hasOwnProperty( k ) ){
  170. var ss = doc.createElement( "style" ),
  171. css = styleBlocks[ k ].join( "\n" );
  172. ss.type = "text/css";
  173. ss.media = k;
  174. //originally, ss was appended to a documentFragment and sheets were appended in bulk.
  175. //this caused crashes in IE in a number of circumstances, such as when the HTML element had a bg image set, so appending beforehand seems best. Thanks to @dvelyk for the initial research on this one!
  176. head.insertBefore( ss, lastLink.nextSibling );
  177. if ( ss.styleSheet ){
  178. ss.styleSheet.cssText = css;
  179. }
  180. else {
  181. ss.appendChild( doc.createTextNode( css ) );
  182. }
  183. //push to appendedEls to track for later removal
  184. appendedEls.push( ss );
  185. }
  186. }
  187. },
  188. //find media blocks in css text, convert to style blocks
  189. translate = function( styles, href, media ){
  190. var qs = styles.replace( respond.regex.comments, '' )
  191. .replace( respond.regex.keyframes, '' )
  192. .match( respond.regex.media ),
  193. ql = qs && qs.length || 0;
  194. //try to get CSS path
  195. href = href.substring( 0, href.lastIndexOf( "/" ) );
  196. var repUrls = function( css ){
  197. return css.replace( respond.regex.urls, "$1" + href + "$2$3" );
  198. },
  199. useMedia = !ql && media;
  200. //if path exists, tack on trailing slash
  201. if( href.length ){ href += "/"; }
  202. //if no internal queries exist, but media attr does, use that
  203. //note: this currently lacks support for situations where a media attr is specified on a link AND
  204. //its associated stylesheet has internal CSS media queries.
  205. //In those cases, the media attribute will currently be ignored.
  206. if( useMedia ){
  207. ql = 1;
  208. }
  209. for( var i = 0; i < ql; i++ ){
  210. var fullq, thisq, eachq, eql;
  211. //media attr
  212. if( useMedia ){
  213. fullq = media;
  214. rules.push( repUrls( styles ) );
  215. }
  216. //parse for styles
  217. else{
  218. fullq = qs[ i ].match( respond.regex.findStyles ) && RegExp.$1;
  219. rules.push( RegExp.$2 && repUrls( RegExp.$2 ) );
  220. }
  221. eachq = fullq.split( "," );
  222. eql = eachq.length;
  223. for( var j = 0; j < eql; j++ ){
  224. thisq = eachq[ j ];
  225. if( isUnsupportedMediaQuery( thisq ) ) {
  226. continue;
  227. }
  228. mediastyles.push( {
  229. media : thisq.split( "(" )[ 0 ].match( respond.regex.only ) && RegExp.$2 || "all",
  230. rules : rules.length - 1,
  231. hasquery : thisq.indexOf("(") > -1,
  232. minw : thisq.match( respond.regex.minw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ),
  233. maxw : thisq.match( respond.regex.maxw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" )
  234. } );
  235. }
  236. }
  237. applyMedia();
  238. },
  239. //recurse through request queue, get css text
  240. makeRequests = function(){
  241. if( requestQueue.length ){
  242. var thisRequest = requestQueue.shift();
  243. ajax( thisRequest.href, function( styles ){
  244. translate( styles, thisRequest.href, thisRequest.media );
  245. parsedSheets[ thisRequest.href ] = true;
  246. // by wrapping recursive function call in setTimeout
  247. // we prevent "Stack overflow" error in IE7
  248. w.setTimeout(function(){ makeRequests(); },0);
  249. } );
  250. }
  251. },
  252. //loop stylesheets, send text content to translate
  253. ripCSS = function(){
  254. for( var i = 0; i < links.length; i++ ){
  255. var sheet = links[ i ],
  256. href = sheet.href,
  257. media = sheet.media,
  258. isCSS = sheet.rel && sheet.rel.toLowerCase() === "stylesheet";
  259. //only links plz and prevent re-parsing
  260. if( !!href && isCSS && !parsedSheets[ href ] ){
  261. // selectivizr exposes css through the rawCssText expando
  262. if (sheet.styleSheet && sheet.styleSheet.rawCssText) {
  263. translate( sheet.styleSheet.rawCssText, href, media );
  264. parsedSheets[ href ] = true;
  265. } else {
  266. if( (!/^([a-zA-Z:]*\/\/)/.test( href ) && !base) ||
  267. href.replace( RegExp.$1, "" ).split( "/" )[0] === w.location.host ){
  268. // IE7 doesn't handle urls that start with '//' for ajax request
  269. // manually add in the protocol
  270. if ( href.substring(0,2) === "//" ) { href = w.location.protocol + href; }
  271. requestQueue.push( {
  272. href: href,
  273. media: media
  274. } );
  275. }
  276. }
  277. }
  278. }
  279. makeRequests();
  280. };
  281. //translate CSS
  282. ripCSS();
  283. //expose update for re-running respond later on
  284. respond.update = ripCSS;
  285. //expose getEmValue
  286. respond.getEmValue = getEmValue;
  287. //adjust on resize
  288. function callMedia(){
  289. applyMedia( true );
  290. }
  291. if( w.addEventListener ){
  292. w.addEventListener( "resize", callMedia, false );
  293. }
  294. else if( w.attachEvent ){
  295. w.attachEvent( "onresize", callMedia );
  296. }
  297. })(this);