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