From b6b8ff2e8123ac6d6181cc877f8b49a81556701e Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Thu, 28 May 2015 11:10:20 +0100 Subject: [PATCH] Update picturefill. --- _assets/javascripts/lib/picturefill.js | 405 ++++++++++++++++--------- 1 file changed, 254 insertions(+), 151 deletions(-) diff --git a/_assets/javascripts/lib/picturefill.js b/_assets/javascripts/lib/picturefill.js index 9e5ccda..d1d86c8 100644 --- a/_assets/javascripts/lib/picturefill.js +++ b/_assets/javascripts/lib/picturefill.js @@ -1,6 +1,6 @@ -/*! Picturefill - v2.1.0 - 2014-08-20 +/*! Picturefill - v2.3.1 - 2015-04-09 * http://scottjehl.github.io/picturefill -* Copyright (c) 2014 https://github.com/scottjehl/picturefill/blob/master/Authors.txt; Licensed MIT */ +* Copyright (c) 2015 https://github.com/scottjehl/picturefill/blob/master/Authors.txt; Licensed MIT */ /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. Dual MIT/BSD license */ window.matchMedia || (window.matchMedia = function() { @@ -52,13 +52,28 @@ window.matchMedia || (window.matchMedia = function() { * License: MIT/GPLv2 * Spec: http://picture.responsiveimages.org/ */ -(function( w, doc ) { +(function( w, doc, image ) { // Enable strict mode "use strict"; + function expose(picturefill) { + /* expose picturefill */ + if ( typeof module === "object" && typeof module.exports === "object" ) { + // CommonJS, just export + module.exports = picturefill; + } else if ( typeof define === "function" && define.amd ) { + // AMD support + define( "picturefill", function() { return picturefill; } ); + } + if ( typeof w === "object" ) { + // If no AMD and we are in the browser, attach to window + w.picturefill = picturefill; + } + } + // If picture is supported, well, that's awesome. Let's get outta here... if ( w.HTMLPictureElement ) { - w.picturefill = function() { }; + expose(function() { }); return; } @@ -66,24 +81,37 @@ window.matchMedia || (window.matchMedia = function() { doc.createElement( "picture" ); // local object for method references and testing exposure - var pf = {}; + var pf = w.picturefill || {}; + + var regWDesc = /\s+\+?\d+(e\d+)?w/; // namespace pf.ns = "picturefill"; // srcset support test - pf.srcsetSupported = "srcset" in doc.createElement( "img" ); - pf.sizesSupported = w.HTMLImageElement.sizes; + (function() { + pf.srcsetSupported = "srcset" in image; + pf.sizesSupported = "sizes" in image; + pf.curSrcSupported = "currentSrc" in image; + })(); // just a string trim workaround pf.trim = function( str ) { return str.trim ? str.trim() : str.replace( /^\s+|\s+$/g, "" ); }; - // just a string endsWith workaround - pf.endsWith = function( str, suffix ) { - return str.endsWith ? str.endsWith( suffix ) : str.indexOf( suffix, str.length - suffix.length ) !== -1; - }; + /** + * Gets a string and returns the absolute URL + * @param src + * @returns {String} absolute URL + */ + pf.makeUrl = (function() { + var anchor = doc.createElement( "a" ); + return function(src) { + anchor.href = src; + return anchor.href; + }; + })(); /** * Shortcut method for https://w3c.github.io/webappsec/specs/mixedcontent/#restricts-mixed-content ( for easy overriding in tests ) @@ -94,13 +122,12 @@ window.matchMedia || (window.matchMedia = function() { /** * Shortcut method for matchMedia ( for easy overriding in tests ) */ + pf.matchesMedia = function( media ) { return w.matchMedia && w.matchMedia( media ).matches; }; - /** - * Shortcut method for `devicePixelRatio` ( for easy overriding in tests ) - */ + // Shortcut method for `devicePixelRatio` ( for easy overriding in tests ) pf.getDpr = function() { return ( w.devicePixelRatio || 1 ); }; @@ -110,87 +137,99 @@ window.matchMedia || (window.matchMedia = function() { * http://dev.w3.org/csswg/css-values-3/#length-value */ pf.getWidthFromLength = function( length ) { - // If a length is specified and doesn’t contain a percentage, and it is greater than 0 or using `calc`, use it. Else, use the `100vw` default. - length = length && length.indexOf( "%" ) > -1 === false && ( parseFloat( length ) > 0 || length.indexOf( "calc(" ) > -1 ) ? length : "100vw"; + var cssValue; + // If a length is specified and doesn’t contain a percentage, and it is greater than 0 or using `calc`, use it. Else, abort. + if ( !(length && length.indexOf( "%" ) > -1 === false && ( parseFloat( length ) > 0 || length.indexOf( "calc(" ) > -1 )) ) { + return false; + } + /** - * If length is specified in `vw` units, use `%` instead since the div we’re measuring - * is injected at the top of the document. - * - * TODO: maybe we should put this behind a feature test for `vw`? - */ + * If length is specified in `vw` units, use `%` instead since the div we’re measuring + * is injected at the top of the document. + * + * TODO: maybe we should put this behind a feature test for `vw`? The risk of doing this is possible browser inconsistancies with vw vs % + */ length = length.replace( "vw", "%" ); // Create a cached element for getting length value widths if ( !pf.lengthEl ) { pf.lengthEl = doc.createElement( "div" ); - doc.documentElement.insertBefore( pf.lengthEl, doc.documentElement.firstChild ); + + // Positioning styles help prevent padding/margin/width on `html` or `body` from throwing calculations off. + pf.lengthEl.style.cssText = "border:0;display:block;font-size:1em;left:0;margin:0;padding:0;position:absolute;visibility:hidden"; + + // Add a class, so that everyone knows where this element comes from + pf.lengthEl.className = "helper-from-picturefill-js"; } - // Positioning styles help prevent padding/margin/width on `html` from throwing calculations off. - pf.lengthEl.style.cssText = "position: absolute; left: 0; width: " + length + ";"; + pf.lengthEl.style.width = "0px"; - if ( pf.lengthEl.offsetWidth <= 0 ) { - // Something has gone wrong. `calc()` is in use and unsupported, most likely. Default to `100vw` (`100%`, for broader support.): - pf.lengthEl.style.cssText = "width: 100%;"; + try { + pf.lengthEl.style.width = length; + } catch ( e ) {} + + doc.body.appendChild(pf.lengthEl); + + cssValue = pf.lengthEl.offsetWidth; + + if ( cssValue <= 0 ) { + cssValue = false; } - return pf.lengthEl.offsetWidth; + doc.body.removeChild( pf.lengthEl ); + + return cssValue; }; + pf.detectTypeSupport = function( type, typeUri ) { + // based on Modernizr's lossless img-webp test + // note: asynchronous + var image = new w.Image(); + image.onerror = function() { + pf.types[ type ] = false; + picturefill(); + }; + image.onload = function() { + pf.types[ type ] = image.width === 1; + picturefill(); + }; + image.src = typeUri; + + return "pending"; + }; // container of supported mime types that one might need to qualify before using - pf.types = {}; + pf.types = pf.types || {}; - // Add support for standard mime types. - pf.types["image/jpeg"] = true; - pf.types["image/gif"] = true; - pf.types["image/png"] = true; + pf.initTypeDetects = function() { + // Add support for standard mime types + pf.types[ "image/jpeg" ] = true; + pf.types[ "image/gif" ] = true; + pf.types[ "image/png" ] = true; + pf.types[ "image/svg+xml" ] = doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1"); + pf.types[ "image/webp" ] = pf.detectTypeSupport("image/webp", "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="); + }; - // test svg support - pf.types[ "image/svg+xml" ] = doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1"); - - // test webp support, only when the markup calls for it - pf.types[ "image/webp" ] = function() { - // based on Modernizr's lossless img-webp test - // note: asynchronous - var img = new w.Image(), - type = "image/webp"; - - img.onerror = function() { - pf.types[ type ] = false; - picturefill(); - }; - img.onload = function() { - pf.types[ type ] = img.width === 1; - picturefill(); - }; - img.src = "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="; - }; - - /** - * Takes a source element and checks if its type attribute is present and if so, supported - * Note: for type tests that require a async logic, - * you can define them as a function that'll run only if that type needs to be tested. Just make the test function call picturefill again when it is complete. - * see the async webp test above for example - */ pf.verifyTypeSupport = function( source ) { var type = source.getAttribute( "type" ); // if type attribute exists, return test result, otherwise return true if ( type === null || type === "" ) { return true; } else { + var pfType = pf.types[ type ]; // if the type test is a function, run it and return "pending" status. The function will rerun picturefill on pending elements once finished. - if ( typeof( pf.types[ type ] ) === "function" ) { - pf.types[ type ](); + if ( typeof pfType === "string" && pfType !== "pending") { + pf.types[ type ] = pf.detectTypeSupport( type, pfType ); + return "pending"; + } else if ( typeof pfType === "function" ) { + pfType(); return "pending"; } else { - return pf.types[ type ]; + return pfType; } } }; - /** - * Parses an individual `size` and returns the length, and optional media query - */ + // Parses an individual `size` and returns the length, and optional media query pf.parseSize = function( sourceSizeStr ) { var match = /(\([^)]+\))?\s*(.+)/g.exec( sourceSizeStr ); return { @@ -199,9 +238,7 @@ window.matchMedia || (window.matchMedia = function() { }; }; - /** - * Takes a string of sizes and returns the width in pixels as a number - */ + // Takes a string of sizes and returns the width in pixels as a number pf.findWidthFromSourceSize = function( sourceSizeListStr ) { // Split up source size list, ie ( max-width: 30em ) 100%, ( max-width: 50em ) 50%, 33% // or (min-width:30em) calc(30% - 15px) @@ -219,34 +256,34 @@ window.matchMedia || (window.matchMedia = function() { if ( !length ) { continue; } - if ( !media || pf.matchesMedia( media ) ) { - // if there is no media query or it matches, choose this as our winning length - // and end algorithm - winningLength = length; + // if there is no media query or it matches, choose this as our winning length + if ( (!media || pf.matchesMedia( media )) && + // pass the length to a method that can properly determine length + // in pixels based on these formats: http://dev.w3.org/csswg/css-values-3/#length-value + (winningLength = pf.getWidthFromLength( length )) ) { break; } } - // pass the length to a method that can properly determine length - // in pixels based on these formats: http://dev.w3.org/csswg/css-values-3/#length-value - return pf.getWidthFromLength( winningLength ); + //if we have no winningLength fallback to 100vw + return winningLength || Math.max(w.innerWidth || 0, doc.documentElement.clientWidth); }; pf.parseSrcset = function( srcset ) { /** - * A lot of this was pulled from Boris Smus’ parser for the now-defunct WHATWG `srcset` - * https://github.com/borismus/srcset-polyfill/blob/master/js/srcset-info.js - * - * 1. Let input (`srcset`) be the value passed to this algorithm. - * 2. Let position be a pointer into input, initially pointing at the start of the string. - * 3. Let raw candidates be an initially empty ordered list of URLs with associated - * unparsed descriptors. The order of entries in the list is the order in which entries - * are added to the list. - */ + * A lot of this was pulled from Boris Smus’ parser for the now-defunct WHATWG `srcset` + * https://github.com/borismus/srcset-polyfill/blob/master/js/srcset-info.js + * + * 1. Let input (`srcset`) be the value passed to this algorithm. + * 2. Let position be a pointer into input, initially pointing at the start of the string. + * 3. Let raw candidates be an initially empty ordered list of URLs with associated + * unparsed descriptors. The order of entries in the list is the order in which entries + * are added to the list. + */ var candidates = []; while ( srcset !== "" ) { - srcset = srcset.replace(/^\s+/g,""); + srcset = srcset.replace( /^\s+/g, "" ); // 5. Collect a sequence of characters that are not space characters, and let that be url. var pos = srcset.search(/\s/g), @@ -255,22 +292,22 @@ window.matchMedia || (window.matchMedia = function() { if ( pos !== -1 ) { url = srcset.slice( 0, pos ); - var last = url[ url.length - 1 ]; + var last = url.slice(-1); // 6. If url ends with a U+002C COMMA character (,), remove that character from url // and let descriptors be the empty string. Otherwise, follow these substeps // 6.1. If url is empty, then jump to the step labeled descriptor parser. if ( last === "," || url === "" ) { - url = url.replace(/,+$/, ""); + url = url.replace( /,+$/, "" ); descriptor = ""; } srcset = srcset.slice( pos + 1 ); - // 6.2. Collect a sequence of characters that are not U+002C COMMA characters (,), and + // 6.2. Collect a sequence of characters that are not U+002C COMMA characters (,), and // let that be descriptors. if ( descriptor === null ) { - var descpos = srcset.indexOf(","); + var descpos = srcset.indexOf( "," ); if ( descpos !== -1 ) { descriptor = srcset.slice( 0, descpos ); srcset = srcset.slice( descpos + 1 ); @@ -296,27 +333,25 @@ window.matchMedia || (window.matchMedia = function() { }; pf.parseDescriptor = function( descriptor, sizesattr ) { - // 11. Descriptor parser: Let candidates be an initially empty source set. The order of entries in the list + // 11. Descriptor parser: Let candidates be an initially empty source set. The order of entries in the list // is the order in which entries are added to the list. var sizes = sizesattr || "100vw", - sizeDescriptor = descriptor && descriptor.replace(/(^\s+|\s+$)/g, ""), + sizeDescriptor = descriptor && descriptor.replace( /(^\s+|\s+$)/g, "" ), widthInCssPixels = pf.findWidthFromSourceSize( sizes ), resCandidate; if ( sizeDescriptor ) { var splitDescriptor = sizeDescriptor.split(" "); - for (var i = splitDescriptor.length + 1; i >= 0; i--) { - if ( splitDescriptor[ i ] !== undefined ) { - var curr = splitDescriptor[ i ], - lastchar = curr && curr.slice( curr.length - 1 ); + for (var i = splitDescriptor.length - 1; i >= 0; i--) { + var curr = splitDescriptor[ i ], + lastchar = curr && curr.slice( curr.length - 1 ); - if ( ( lastchar === "h" || lastchar === "w" ) && !pf.sizesSupported ) { - resCandidate = parseFloat( ( parseInt( curr, 10 ) / widthInCssPixels ) ); - } else if ( lastchar === "x" ) { - var res = curr && parseFloat( curr, 10 ); - resCandidate = res && !isNaN( res ) ? res : 1; - } + if ( ( lastchar === "h" || lastchar === "w" ) && !pf.sizesSupported ) { + resCandidate = parseFloat( ( parseInt( curr, 10 ) / widthInCssPixels ) ); + } else if ( lastchar === "x" ) { + var res = curr && parseFloat( curr, 10 ); + resCandidate = res && !isNaN( res ) ? res : 1; } } } @@ -348,7 +383,7 @@ window.matchMedia || (window.matchMedia = function() { return formattedCandidates; }; - /* + /** * if it's an img element and it has a srcset property, * we need to remove the attribute so we can manipulate src * (the property's existence infers native srcset support, and a srcset-supporting browser will prioritize srcset's value over our winning picture candidate) @@ -357,13 +392,12 @@ window.matchMedia || (window.matchMedia = function() { pf.dodgeSrcset = function( img ) { if ( img.srcset ) { img[ pf.ns ].srcset = img.srcset; - img.removeAttribute( "srcset" ); + img.srcset = ""; + img.setAttribute( "data-pfsrcset", img[ pf.ns ].srcset ); } }; - /* - * Accept a source or img element and process its srcset and sizes attrs - */ + // Accept a source or img element and process its srcset and sizes attrs pf.processSourceSet = function( el ) { var srcset = el.getAttribute( "srcset" ), sizes = el.getAttribute( "sizes" ), @@ -380,6 +414,66 @@ window.matchMedia || (window.matchMedia = function() { return candidates; }; + pf.backfaceVisibilityFix = function( picImg ) { + // See: https://github.com/scottjehl/picturefill/issues/332 + var style = picImg.style || {}, + WebkitBackfaceVisibility = "webkitBackfaceVisibility" in style, + currentZoom = style.zoom; + + if (WebkitBackfaceVisibility) { + style.zoom = ".999"; + + WebkitBackfaceVisibility = picImg.offsetWidth; + + style.zoom = currentZoom; + } + }; + + pf.setIntrinsicSize = (function() { + var urlCache = {}; + var setSize = function( picImg, width, res ) { + if ( width ) { + picImg.setAttribute( "width", parseInt(width / res, 10) ); + } + }; + return function( picImg, bestCandidate ) { + var img; + if ( !picImg[ pf.ns ] || w.pfStopIntrinsicSize ) { + return; + } + if ( picImg[ pf.ns ].dims === undefined ) { + picImg[ pf.ns].dims = picImg.getAttribute("width") || picImg.getAttribute("height"); + } + if ( picImg[ pf.ns].dims ) { return; } + + if ( bestCandidate.url in urlCache ) { + setSize( picImg, urlCache[bestCandidate.url], bestCandidate.resolution ); + } else { + img = doc.createElement( "img" ); + img.onload = function() { + urlCache[bestCandidate.url] = img.width; + + //IE 10/11 don't calculate width for svg outside document + if ( !urlCache[bestCandidate.url] ) { + try { + doc.body.appendChild( img ); + urlCache[bestCandidate.url] = img.width || img.offsetWidth; + doc.body.removeChild( img ); + } catch(e){} + } + + if ( picImg.src === bestCandidate.url ) { + setSize( picImg, urlCache[bestCandidate.url], bestCandidate.resolution ); + } + picImg = null; + img.onload = null; + img = null; + }; + img.src = bestCandidate.url; + } + }; + })(); + pf.applyBestCandidate = function( candidates, picImg ) { var candidate, length, @@ -398,17 +492,28 @@ window.matchMedia || (window.matchMedia = function() { } } - if ( bestCandidate && !pf.endsWith( picImg.src, bestCandidate.url ) ) { - if ( pf.restrictsMixedContent() && bestCandidate.url.substr(0, "http:".length).toLowerCase() === "http:" ) { - if ( typeof console !== undefined ) { - console.warn( "Blocked mixed content image " + bestCandidate.url ); + if ( bestCandidate ) { + + bestCandidate.url = pf.makeUrl( bestCandidate.url ); + + if ( picImg.src !== bestCandidate.url ) { + if ( pf.restrictsMixedContent() && bestCandidate.url.substr(0, "http:".length).toLowerCase() === "http:" ) { + if ( window.console !== undefined ) { + console.warn( "Blocked mixed content image " + bestCandidate.url ); + } + } else { + picImg.src = bestCandidate.url; + // currentSrc attribute and property to match + // http://picture.responsiveimages.org/#the-img-element + if ( !pf.curSrcSupported ) { + picImg.currentSrc = picImg.src; + } + + pf.backfaceVisibilityFix( picImg ); } - } else { - picImg.src = bestCandidate.url; - // currentSrc attribute and property to match - // http://picture.responsiveimages.org/#the-img-element - picImg.currentSrc = picImg.src; } + + pf.setIntrinsicSize(picImg, bestCandidate); } }; @@ -416,7 +521,7 @@ window.matchMedia || (window.matchMedia = function() { return a.resolution - b.resolution; }; - /* + /** * In IE9, elements get removed if they aren't children of * video elements. Thus, we conditionally wrap source elements * using @@ -436,7 +541,7 @@ window.matchMedia || (window.matchMedia = function() { } }; - /* + /** * Find all `img` elements, and add them to the candidate list if they have * a `picture` parent, a `sizes` attribute in basic `srcset` supporting browsers, * a `srcset` attribute at all, and they haven’t been evaluated already. @@ -449,8 +554,8 @@ window.matchMedia || (window.matchMedia = function() { var currImg = imgs[ h ]; if ( currImg.parentNode.nodeName.toUpperCase() === "PICTURE" || - ( currImg.getAttribute( "srcset" ) !== null ) || currImg[ pf.ns ] && currImg[ pf.ns ].srcset !== null ) { - elems.push( currImg ); + ( currImg.getAttribute( "srcset" ) !== null ) || currImg[ pf.ns ] && currImg[ pf.ns ].srcset !== null ) { + elems.push( currImg ); } } return elems; @@ -480,7 +585,7 @@ window.matchMedia || (window.matchMedia = function() { continue; } // if it's a source element that has the `src` property set, throw a warning in the console - if ( source.getAttribute( "src" ) !== null && typeof console !== undefined ){ + if ( source.getAttribute( "src" ) !== null && typeof console !== undefined ) { console.warn("The `src` attribute is invalid on `picture` `source` element; instead, use `srcset`."); } @@ -513,8 +618,8 @@ window.matchMedia || (window.matchMedia = function() { parent, firstMatch, candidates, + options = opt || {}; - options = opt || {}; elements = options.elements || pf.getAllElements(); // Loop through all elements @@ -524,20 +629,25 @@ window.matchMedia || (window.matchMedia = function() { firstMatch = undefined; candidates = undefined; + // immediately skip non-`img` nodes + if ( element.nodeName.toUpperCase() !== "IMG" ) { + continue; + } + // expando for caching data on the img if ( !element[ pf.ns ] ) { element[ pf.ns ] = {}; } - // if the element has already been evaluated, skip it - // unless `options.force` is set to true ( this, for example, + // if the element has already been evaluated, skip it unless + // `options.reevaluate` is set to true ( this, for example, // is set to true when running `picturefill` on `resize` ). if ( !options.reevaluate && element[ pf.ns ].evaluated ) { continue; } // if `img` is in a `picture` element - if ( parent.nodeName.toUpperCase() === "PICTURE" ) { + if ( parent && parent.nodeName.toUpperCase() === "PICTURE" ) { // IE9 video workaround pf.removeVideoShim( parent ); @@ -558,9 +668,8 @@ window.matchMedia || (window.matchMedia = function() { } // Cache and remove `srcset` if present and we’re going to be doing `picture`/`srcset`/`sizes` polyfilling to it. - if ( parent.nodeName.toUpperCase() === "PICTURE" || - ( element.srcset && !pf.srcsetSupported ) || - ( !pf.sizesSupported && ( element.srcset && element.srcset.indexOf("w") > -1 ) ) ) { + if ( ( parent && parent.nodeName.toUpperCase() === "PICTURE" ) || + ( !pf.sizesSupported && ( element.srcset && regWDesc.test( element.srcset ) ) ) ) { pf.dodgeSrcset( element ); } @@ -588,28 +697,32 @@ window.matchMedia || (window.matchMedia = function() { * Also attaches picturefill on resize */ function runPicturefill() { + pf.initTypeDetects(); picturefill(); var intervalId = setInterval( function() { // When the document has finished loading, stop checking for new images // https://github.com/ded/domready/blob/master/ready.js#L15 picturefill(); + if ( /^loaded|^i|^c/.test( doc.readyState ) ) { clearInterval( intervalId ); return; } }, 250 ); + + var resizeTimer; + var handleResize = function() { + picturefill({ reevaluate: true }); + }; + function checkResize() { + clearTimeout(resizeTimer); + resizeTimer = setTimeout( handleResize, 60 ); + } + if ( w.addEventListener ) { - var resizeThrottle; - w.addEventListener( "resize", function() { - if (!w._picturefillWorking) { - w._picturefillWorking = true; - w.clearTimeout( resizeThrottle ); - resizeThrottle = w.setTimeout( function() { - picturefill({ reevaluate: true }); - w._picturefillWorking = false; - }, 60 ); - } - }, false ); + w.addEventListener( "resize", checkResize, false ); + } else if ( w.attachEvent ) { + w.attachEvent( "onresize", checkResize ); } } @@ -618,16 +731,6 @@ window.matchMedia || (window.matchMedia = function() { /* expose methods for testing */ picturefill._ = pf; - /* expose picturefill */ - if ( typeof module === "object" && typeof module.exports === "object" ) { - // CommonJS, just export - module.exports = picturefill; - } else if ( typeof define === "function" && define.amd ){ - // AMD support - define( function() { return picturefill; } ); - } else if ( typeof w === "object" ) { - // If no AMD and we are in the browser, attach to window - w.picturefill = picturefill; - } + expose( picturefill ); -} )( this, this.document ); +} )( window, window.document, new window.Image() );