diff --git a/core/assets/vendor/picturefill/picturefill.js b/core/assets/vendor/picturefill/picturefill.js index 4d69569..c72991f 100644 --- a/core/assets/vendor/picturefill/picturefill.js +++ b/core/assets/vendor/picturefill/picturefill.js @@ -1,126 +1,659 @@ -/*jshint loopfunc: true, browser: true, curly: true, eqeqeq: true, expr: true, forin: true, latedef: true, newcap: true, noarg: true, trailing: true, undef: true, unused: true */ -/*! Picturefill - Author: Scott Jehl, 2012 | License: MIT/GPLv2 */ -(function (w) { - - // Enable strict mode. - "use strict"; - - // Test if `` is supported natively, if so, exit. - if (!!(w.document.createElement('picture') && w.document.createElement('source') && w.HTMLPictureElement)) { - return; - } - - w.picturefill = function () { - // Copy attributes from the source to the destination. - function _copyAttributes(src, tar) { - if (src.getAttribute('width') && src.getAttribute('height')) { - tar.width = src.getAttribute('width'); - tar.height = src.getAttribute('height'); - } - } - - // Get all picture tags. - var ps = w.document.getElementsByTagName('picture'); - - // Loop the pictures. - for (var i = 0, il = ps.length; i < il; i++) { - var sources = ps[i].getElementsByTagName('source'); - var picImg = null; - var matches = []; - - // If no sources are found, they're likely erased from the DOM. - // Try finding them inside comments. - if (!sources.length) { - var picText = ps[i].innerHTML; - var frag = w.document.createElement('div'); - // For IE9, convert the source elements to divs. - var srcs = picText.replace(/(<)source([^>]+>)/gmi, '$1div$2').match(/]+>/gmi); - - frag.innerHTML = srcs.join(''); - sources = frag.getElementsByTagName('div'); - } - - // See which sources match. - for (var j = 0, jl = sources.length; j < jl; j++) { - var media = sources[j].getAttribute('media'); - // If there's no media specified or the media query matches, add it. - if (!media || (w.matchMedia && w.matchMedia(media).matches)) { - matches.push(sources[j]); - } - } - - if (matches.length) { - // Grab the most appropriate (last) match. - var match = matches.pop(); - var srcset = match.getAttribute('srcset'); - - // Find any existing img element in the picture element. - picImg = ps[i].getElementsByTagName('img')[0]; - - // Add a new img element if one doesn't exists. - if (!picImg) { - picImg = w.document.createElement('img'); - picImg.alt = ps[i].getAttribute('alt'); - ps[i].appendChild(picImg); - } - - // Source element uses a srcset. - if (srcset) { - var screenRes = w.devicePixelRatio || 1; - // Split comma-separated `srcset` sources into an array. - sources = srcset.split(', '); - - // Loop through each source/resolution in srcset. - for (var res = sources.length, r = res - 1; r >= 0; r--) { - // Remove any leading whitespace, then split on spaces. - var source = sources[ r ].replace(/^\s*/, '').replace(/\s*$/, '').split(' '); - // Parse out the resolution for each source in `srcset`. - var resMatch = parseFloat(source[1], 10); - - if (screenRes >= resMatch) { - if (picImg.getAttribute('src') !== source[0]) { - var newImg = document.createElement('img'); - - newImg.src = source[0]; - // When the image is loaded, set a width equal to that of the - // original’s intrinsic width divided by the screen resolution. - newImg.onload = function () { - // Clone the original image into memory so the width is - // unaffected by page styles. - var w = this.cloneNode(true).width; - if (w > 0) { - this.width = (w / resMatch); - } - }; - // Copy width and height from the source tag to the img element. - _copyAttributes(match, newImg); - picImg.parentNode.replaceChild(newImg, picImg); - } - // We’ve matched, so bail out of the loop here. - break; - } - } - } else { - // No srcset used, so just use the 'src' value. - picImg.src = match.getAttribute('src'); - // Copy width and height from the source tag to the img element. - _copyAttributes(match, picImg); - } - } - } - }; - - // Run on resize and domready (w.load as a fallback) - if (w.addEventListener) { - w.addEventListener('resize', w.picturefill, false); - w.addEventListener('DOMContentLoaded', function () { - w.picturefill(); - // Run once only. - w.removeEventListener('load', w.picturefill, false); - }, false); - w.addEventListener('load', w.picturefill, false); - } - else if (w.attachEvent) { - w.attachEvent('onload', w.picturefill); - } -})(this); +/*! Picturefill - v2.2.0 - 2014-10-30 +* http://scottjehl.github.io/picturefill +* Copyright (c) 2014 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() { + "use strict"; + + // For browsers that support matchMedium api such as IE 9 and webkit + var styleMedia = (window.styleMedia || window.media); + + // For those that don't support matchMedium + if (!styleMedia) { + var style = document.createElement('style'), + script = document.getElementsByTagName('script')[0], + info = null; + + style.type = 'text/css'; + style.id = 'matchmediajs-test'; + + script.parentNode.insertBefore(style, script); + + // 'style.currentStyle' is used by IE <= 8 and 'window.getComputedStyle' for all other browsers + info = ('getComputedStyle' in window) && window.getComputedStyle(style, null) || style.currentStyle; + + styleMedia = { + matchMedium: function(media) { + var text = '@media ' + media + '{ #matchmediajs-test { width: 1px; } }'; + + // 'style.styleSheet' is used by IE <= 8 and 'style.textContent' for all other browsers + if (style.styleSheet) { + style.styleSheet.cssText = text; + } else { + style.textContent = text; + } + + // Test if media query is true or false + return info.width === '1px'; + } + }; + } + + return function(media) { + return { + matches: styleMedia.matchMedium(media || 'all'), + media: media || 'all' + }; + }; +}()); +/*! Picturefill - Responsive Images that work today. +* Author: Scott Jehl, Filament Group, 2012 ( new proposal implemented by Shawn Jansepar ) +* License: MIT/GPLv2 +* Spec: http://picture.responsiveimages.org/ +*/ +(function( w, doc, image ) { + // Enable strict mode + "use strict"; + + // If picture is supported, well, that's awesome. Let's get outta here... + if ( w.HTMLPictureElement ) { + w.picturefill = function() { }; + return; + } + + // HTML shim|v it for old IE (IE9 will still need the HTML video tag workaround) + doc.createElement( "picture" ); + + // local object for method references and testing exposure + var pf = {}; + + // namespace + pf.ns = "picturefill"; + + // srcset support test + (function() { + pf.srcsetSupported = "srcset" in image; + pf.sizesSupported = "sizes" 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; + }; + + /** + * Shortcut method for https://w3c.github.io/webappsec/specs/mixedcontent/#restricts-mixed-content ( for easy overriding in tests ) + */ + pf.restrictsMixedContent = function() { + return w.location.protocol === "https:"; + }; + /** + * 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 ) + pf.getDpr = function() { + return ( w.devicePixelRatio || 1 ); + }; + + /** + * Get width in css pixel value from a "length" value + * 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"; + + /** + * 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`? + */ + length = length.replace( "vw", "%" ); + + // Create a cached element for getting length value widths + if ( !pf.lengthEl ) { + pf.lengthEl = doc.createElement( "div" ); + + // 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"; + } + + pf.lengthEl.style.width = length; + + doc.body.appendChild(pf.lengthEl); + + // Add a class, so that everyone knows where this element comes from + pf.lengthEl.className = "helper-from-picturefill-js"; + + 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.width = doc.documentElement.offsetWidth + "px"; + } + + var offsetWidth = pf.lengthEl.offsetWidth; + + doc.body.removeChild( pf.lengthEl ); + + return offsetWidth; + }; + + // container of supported mime types that one might need to qualify before using + pf.types = {}; + + // Add support for standard mime types + pf.types[ "image/jpeg" ] = true; + pf.types[ "image/gif" ] = true; + pf.types[ "image/png" ] = true; + + // 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 type = "image/webp"; + + image.onerror = function() { + pf.types[ type ] = false; + picturefill(); + }; + image.onload = function() { + pf.types[ type ] = image.width === 1; + picturefill(); + }; + image.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 { + // 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 ](); + return "pending"; + } else { + return pf.types[ type ]; + } + } + }; + + // Parses an individual `size` and returns the length, and optional media query + pf.parseSize = function( sourceSizeStr ) { + var match = /(\([^)]+\))?\s*(.+)/g.exec( sourceSizeStr ); + return { + media: match && match[1], + length: match && match[2] + }; + }; + + // 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) + var sourceSizeList = pf.trim( sourceSizeListStr ).split( /\s*,\s*/ ), + winningLength; + + for ( var i = 0, len = sourceSizeList.length; i < len; i++ ) { + // Match ? length, ie ( min-width: 50em ) 100% + var sourceSize = sourceSizeList[ i ], + // Split "( min-width: 50em ) 100%" into separate strings + parsedSize = pf.parseSize( sourceSize ), + length = parsedSize.length, + media = parsedSize.media; + + 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; + 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 ); + }; + + 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. + */ + var candidates = []; + + while ( srcset !== "" ) { + 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), + url, descriptor = null; + + if ( pos !== -1 ) { + url = srcset.slice( 0, pos ); + + 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( /,+$/, "" ); + descriptor = ""; + } + srcset = srcset.slice( pos + 1 ); + + // 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( "," ); + if ( descpos !== -1 ) { + descriptor = srcset.slice( 0, descpos ); + srcset = srcset.slice( descpos + 1 ); + } else { + descriptor = srcset; + srcset = ""; + } + } + } else { + url = srcset; + srcset = ""; + } + + // 7. Add url to raw candidates, associated with descriptors. + if ( url || descriptor ) { + candidates.push({ + url: url, + descriptor: descriptor + }); + } + } + return candidates; + }; + + pf.parseDescriptor = function( descriptor, sizesattr ) { + // 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, "" ), + widthInCssPixels = pf.findWidthFromSourceSize( sizes ), + resCandidate; + + if ( sizeDescriptor ) { + var splitDescriptor = sizeDescriptor.split(" "); + + 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; + } + } + } + return resCandidate || 1; + }; + + /** + * Takes a srcset in the form of url/ + * ex. "images/pic-medium.png 1x, images/pic-medium-2x.png 2x" or + * "images/pic-medium.png 400w, images/pic-medium-2x.png 800w" or + * "images/pic-small.png" + * Get an array of image candidates in the form of + * {url: "/foo/bar.png", resolution: 1} + * where resolution is http://dev.w3.org/csswg/css-values-3/#resolution-value + * If sizes is specified, resolution is calculated + */ + pf.getCandidatesFromSourceSet = function( srcset, sizes ) { + var candidates = pf.parseSrcset( srcset ), + formattedCandidates = []; + + for ( var i = 0, len = candidates.length; i < len; i++ ) { + var candidate = candidates[ i ]; + + formattedCandidates.push({ + url: candidate.url, + resolution: pf.parseDescriptor( candidate.descriptor, sizes ) + }); + } + 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) + * this moves srcset's value to memory for later use and removes the attr + */ + pf.dodgeSrcset = function( img ) { + if ( img.srcset ) { + img[ pf.ns ].srcset = img.srcset; + img.removeAttribute( "srcset" ); + } + }; + + // 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" ), + candidates = []; + + // if it's an img element, use the cached srcset property (defined or not) + if ( el.nodeName.toUpperCase() === "IMG" && el[ pf.ns ] && el[ pf.ns ].srcset ) { + srcset = el[ pf.ns ].srcset; + } + + if ( srcset ) { + candidates = pf.getCandidatesFromSourceSet( srcset, sizes ); + } + return candidates; + }; + + pf.applyBestCandidate = function( candidates, picImg ) { + var candidate, + length, + bestCandidate; + + candidates.sort( pf.ascendingSort ); + + length = candidates.length; + bestCandidate = candidates[ length - 1 ]; + + for ( var i = 0; i < length; i++ ) { + candidate = candidates[ i ]; + if ( candidate.resolution >= pf.getDpr() ) { + bestCandidate = candidate; + break; + } + } + + 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 ); + } + } else { + picImg.src = bestCandidate.url; + // currentSrc attribute and property to match + // http://picture.responsiveimages.org/#the-img-element + picImg.currentSrc = picImg.src; + + var style = picImg.style || {}, + WebkitBackfaceVisibility = "webkitBackfaceVisibility" in style, + currentZoom = style.zoom; + + if (WebkitBackfaceVisibility) { // See: https://github.com/scottjehl/picturefill/issues/332 + style.zoom = ".999"; + + WebkitBackfaceVisibility = picImg.offsetWidth; + + style.zoom = currentZoom; + } + } + } + }; + + pf.ascendingSort = function( a, b ) { + 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 + * and must account for that here by moving those source elements + * back into the picture element. + */ + pf.removeVideoShim = function( picture ) { + var videos = picture.getElementsByTagName( "video" ); + if ( videos.length ) { + var video = videos[ 0 ], + vsources = video.getElementsByTagName( "source" ); + while ( vsources.length ) { + picture.insertBefore( vsources[ 0 ], video ); + } + // Remove the video element once we're finished removing its children + video.parentNode.removeChild( video ); + } + }; + + /** + * 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. + */ + pf.getAllElements = function() { + var elems = [], + imgs = doc.getElementsByTagName( "img" ); + + for ( var h = 0, len = imgs.length; h < len; h++ ) { + 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 ); + } + } + return elems; + }; + + pf.getMatch = function( img, picture ) { + var sources = picture.childNodes, + match; + + // Go through each child, and if they have media queries, evaluate them + for ( var j = 0, slen = sources.length; j < slen; j++ ) { + var source = sources[ j ]; + + // ignore non-element nodes + if ( source.nodeType !== 1 ) { + continue; + } + + // Hitting the `img` element that started everything stops the search for `sources`. + // If no previous `source` matches, the `img` itself is evaluated later. + if ( source === img ) { + return match; + } + + // ignore non-`source` nodes + if ( source.nodeName.toUpperCase() !== "SOURCE" ) { + 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 ) { + console.warn("The `src` attribute is invalid on `picture` `source` element; instead, use `srcset`."); + } + + var media = source.getAttribute( "media" ); + + // if source does not have a srcset attribute, skip + if ( !source.getAttribute( "srcset" ) ) { + continue; + } + + // if there's no media specified, OR w.matchMedia is supported + if ( ( !media || pf.matchesMedia( media ) ) ) { + var typeSupported = pf.verifyTypeSupport( source ); + + if ( typeSupported === true ) { + match = source; + break; + } else if ( typeSupported === "pending" ) { + return false; + } + } + } + + return match; + }; + + function picturefill( opt ) { + var elements, + element, + parent, + firstMatch, + candidates, + options = opt || {}; + + elements = options.elements || pf.getAllElements(); + + // Loop through all elements + for ( var i = 0, plen = elements.length; i < plen; i++ ) { + element = elements[ i ]; + parent = element.parentNode; + 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.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" ) { + + // IE9 video workaround + pf.removeVideoShim( parent ); + + // return the first match which might undefined + // returns false if there is a pending source + // TODO the return type here is brutal, cleanup + firstMatch = pf.getMatch( element, parent ); + + // if any sources are pending in this picture due to async type test(s) + // remove the evaluated attr and skip for now ( the pending test will + // rerun picturefill on this element when complete) + if ( firstMatch === false ) { + continue; + } + } else { + firstMatch = undefined; + } + + // 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 ) ) ) { + pf.dodgeSrcset( element ); + } + + if ( firstMatch ) { + candidates = pf.processSourceSet( firstMatch ); + pf.applyBestCandidate( candidates, element ); + } else { + // No sources matched, so we’re down to processing the inner `img` as a source. + candidates = pf.processSourceSet( element ); + + if ( element.srcset === undefined || element[ pf.ns ].srcset ) { + // Either `srcset` is completely unsupported, or we need to polyfill `sizes` functionality. + pf.applyBestCandidate( candidates, element ); + } // Else, resolution-only `srcset` is supported natively. + } + + // set evaluated to true to avoid unnecessary reparsing + element[ pf.ns ].evaluated = true; + } + } + + /** + * Sets up picture polyfill by polling the document and running + * the polyfill every 250ms until the document is ready. + * Also attaches picturefill on resize + */ + function runPicturefill() { + 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 ); + + function checkResize() { + var resizeThrottle; + + if ( !w._picturefillWorking ) { + w._picturefillWorking = true; + w.clearTimeout( resizeThrottle ); + resizeThrottle = w.setTimeout( function() { + picturefill({ reevaluate: true }); + w._picturefillWorking = false; + }, 60 ); + } + } + + if ( w.addEventListener ) { + w.addEventListener( "resize", checkResize, false ); + } else if ( w.attachEvent ) { + w.attachEvent( "onresize", checkResize ); + } + } + + runPicturefill(); + + /* 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; + } + +} )( this, this.document, new this.Image() ); diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 41406af..bc9f3df 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -776,12 +776,10 @@ normalize: picturefill: remote: https://github.com/scottjehl/picturefill - # @todo Contribute upstream and/or replace with upstream version. - # @see https://drupal.org/node/1775530 - version: VERSION + version: 2.2.0 license: name: MIT - url: https://github.com/scottjehl/picturefill/blob/master/LICENSE + url: https://github.com/scottjehl/picturefill/blob/2.2.0/LICENSE gpl-compatible: true js: assets/vendor/picturefill/picturefill.js: { weight: -10 } diff --git a/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php b/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php index a8a46f9..83986eb 100644 --- a/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php +++ b/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php @@ -33,54 +33,54 @@ protected function setUp() { public function testThemeBreakpoints() { // Verify the breakpoint group for breakpoint_theme_test was created. $expected_breakpoints = array( - 'breakpoint_theme_test.mobile' => array( - 'label' => 'mobile', - 'mediaQuery' => '(min-width: 0px)', - 'weight' => 0, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.mobile', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'breakpoint_theme_test.tv' => array( + 'label' => 'tv', + 'mediaQuery' => 'only screen and (min-width: 1220px)', + 'weight' => 0, + 'multipliers' => array( + '1x', ), - 'breakpoint_theme_test.narrow' => array( - 'label' => 'narrow', - 'mediaQuery' => '(min-width: 560px)', - 'weight' => 1, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.narrow', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.tv', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), + 'breakpoint_theme_test.wide' => array( + 'label' => 'wide', + 'mediaQuery' => '(min-width: 851px)', + 'weight' => 1, + 'multipliers' => array( + '1x', ), - 'breakpoint_theme_test.wide' => array( - 'label' => 'wide', - 'mediaQuery' => '(min-width: 851px)', - 'weight' => 2, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.wide', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.wide', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), + 'breakpoint_theme_test.narrow' => array( + 'label' => 'narrow', + 'mediaQuery' => '(min-width: 560px)', + 'weight' => 2, + 'multipliers' => array( + '1x', ), - 'breakpoint_theme_test.tv' => array( - 'label' => 'tv', - 'mediaQuery' => 'only screen and (min-width: 3456px)', - 'weight' => 3, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.tv', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.narrow', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), + 'breakpoint_theme_test.mobile' => array( + 'label' => 'mobile', + 'mediaQuery' => '(min-width: 0px)', + 'weight' => 3, + 'multipliers' => array( + '1x', ), + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.mobile', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), ); $breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup('breakpoint_theme_test'); @@ -101,7 +101,7 @@ public function testCustomBreakpointGroups () { 'breakpoint_theme_test.group2.narrow' => array( 'label' => 'narrow', 'mediaQuery' => '(min-width: 560px)', - 'weight' => 1, + 'weight' => 2, 'multipliers' => array( '1x', '2x', @@ -114,7 +114,7 @@ public function testCustomBreakpointGroups () { 'breakpoint_theme_test.group2.wide' => array( 'label' => 'wide', 'mediaQuery' => '(min-width: 851px)', - 'weight' => 2, + 'weight' => 1, 'multipliers' => array( '1x', '2x', @@ -127,7 +127,7 @@ public function testCustomBreakpointGroups () { 'breakpoint_module_test.breakpoint_theme_test.group2.tv' => array( 'label' => 'tv', 'mediaQuery' => '(min-width: 6000px)', - 'weight' => 3, + 'weight' => 0, 'multipliers' => array( '1x', ), @@ -152,7 +152,7 @@ public function testModuleBreakpoints() { 'breakpoint_module_test.mobile' => array( 'label' => 'mobile', 'mediaQuery' => '(min-width: 0px)', - 'weight' => 0, + 'weight' => 1, 'multipliers' => array( '1x', ), @@ -164,7 +164,7 @@ public function testModuleBreakpoints() { 'breakpoint_module_test.standard' => array( 'label' => 'standard', 'mediaQuery' => '(min-width: 560px)', - 'weight' => 1, + 'weight' => 0, 'multipliers' => array( '1x', '2x', diff --git a/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml b/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml index 0c24709..e1edeb2 100644 --- a/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml +++ b/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml @@ -1,12 +1,12 @@ breakpoint_module_test.mobile: label: mobile mediaQuery: '(min-width: 0px)' - weight: 0 + weight: 1 # Don't include multipliers. A 1x multiplier this will be enforced by default. breakpoint_module_test.standard: label: standard mediaQuery: '(min-width: 560px)' - weight: 1 + weight: 0 # Don't include a 1x multiplier this will be enforced by default. multipliers: - 2x @@ -15,7 +15,7 @@ breakpoint_module_test.standard: breakpoint_module_test.breakpoint_theme_test.group2.tv: label: tv mediaQuery: '(min-width: 6000px)' - weight: 3 + weight: 0 multipliers: - 1x group: breakpoint_theme_test.group2 diff --git a/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php b/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php index f4b2a0b..fea2d8f 100644 --- a/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php +++ b/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php @@ -87,9 +87,9 @@ public function testGetWeight() { * @covers ::getMediaQuery */ public function testGetMediaQuery() { - $this->pluginDefinition['mediaQuery'] = 'only screen and (min-width: 3456px)'; + $this->pluginDefinition['mediaQuery'] = 'only screen and (min-width: 1220px)'; $this->setupBreakpoint(); - $this->assertEquals('only screen and (min-width: 3456px)', $this->breakpoint->getMediaQuery()); + $this->assertEquals('only screen and (min-width: 1220px)', $this->breakpoint->getMediaQuery()); } /** diff --git a/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml b/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml index 6d34ec7..3e565e1 100644 --- a/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml +++ b/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml @@ -1,32 +1,32 @@ breakpoint_theme_test.mobile: label: mobile mediaQuery: '(min-width: 0px)' - weight: 0 + weight: 3 multipliers: - 1x breakpoint_theme_test.narrow: label: narrow mediaQuery: '(min-width: 560px)' - weight: 1 + weight: 2 multipliers: - 1x # Out of order breakpoint to test sorting. breakpoint_theme_test.tv: label: tv - mediaQuery: 'only screen and (min-width: 3456px)' - weight: 3 + mediaQuery: 'only screen and (min-width: 1220px)' + weight: 0 multipliers: - 1x breakpoint_theme_test.wide: label: wide mediaQuery: '(min-width: 851px)' - weight: 2 + weight: 1 multipliers: - 1x breakpoint_theme_test.group2.narrow: label: narrow mediaQuery: '(min-width: 560px)' - weight: 1 + weight: 2 multipliers: - 1x - 2x @@ -34,7 +34,7 @@ breakpoint_theme_test.group2.narrow: breakpoint_theme_test.group2.wide: label: wide mediaQuery: '(min-width: 851px)' - weight: 2 + weight: 1 multipliers: - 1x - 2x diff --git a/core/modules/responsive_image/config/schema/responsive_image.schema.yml b/core/modules/responsive_image/config/schema/responsive_image.schema.yml index 52fc946..82b731c 100644 --- a/core/modules/responsive_image/config/schema/responsive_image.schema.yml +++ b/core/modules/responsive_image/config/schema/responsive_image.schema.yml @@ -20,6 +20,12 @@ responsive_image.mappings.*: - type: mapping label: 'Mapping' mapping: + # Image mapping type. Either 'sizes' (using the 'sizes' attribute) + # or 'image_style' (using a single image style to map to this + # breakpoint). + image_mapping_type: + type: string + label: 'Responsive image mapping type' breakpoint_id: type: string label: 'Breakpoint ID' @@ -29,6 +35,17 @@ responsive_image.mappings.*: image_style: type: string label: 'Image style' + # The value for the sizes attribute as described in the spec: + # http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-sizes + sizes: + type: string + label: 'Sizes attribute' + sizes_image_styles: + type: sequence + label: 'Image styles to be used when using the ''sizes'' attribute' + sequence: + - type: string + label: 'Image style' breakpointGroup: type: string label: 'Breakpoint group' diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 600ce73..351c8b7 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -9,8 +9,10 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Routing\RouteMatchInterface; use \Drupal\Core\Template\Attribute; +use Drupal\image\Entity\ImageStyle; use Drupal\Core\Url; use Drupal\responsive_image\Entity\ResponsiveImageMapping; +use Drupal\Core\Image\ImageInterface; /** * The machine name for the empty image breakpoint image style option. @@ -96,12 +98,11 @@ function responsive_image_theme() { ), 'responsive_image_source' => array( 'variables' => array( - 'src' => NULL, - 'srcset' => NULL, - 'dimensions' => NULL, + 'srcset' => array(), 'media' => NULL, + 'mime_type' => NULL, + 'sizes' => NULL, ), - 'function' => 'theme_responsive_image_source', ), ); } @@ -189,114 +190,255 @@ function theme_responsive_image($variables) { unset($variables['height']); } - $sources = array(); - - // Fallback image, output as source with media query. - $sources[] = array( - 'src' => _responsive_image_image_style_url($variables['style_name'], $variables['uri']), - 'dimensions' => responsive_image_get_image_dimensions($variables), + $image = \Drupal::service('image.factory')->get($variables['uri']); + $sources = responsive_image_build_source_tags($image, $variables); + $output = array(); + $output[] = ''; + if (!empty($sources)) { + // Internet Explorer 9 doesn't recognise source elements that are wrapped in + // picture tags. See http://scottjehl.github.io/picturefill/#ie9 + $output[] = ''; + foreach ($sources as $source) { + $output[] = drupal_render($source); + } + $output[] = ''; + } + // Output the fallback image. Use srcset in the fallback image to avoid + // unnecessary preloading of images in older browsers. See + // http://scottjehl.github.io/picturefill/#using-picture and + // http://scottjehl.github.io/picturefill/#gotchas for more information. + $fallback_image = array( + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => _responsive_image_image_style_url($variables['style_name'], $image->getSource()), + ), + ), ); + foreach (array('alt', 'title', 'attributes') as $key) { + if (isset($variables[$key])) { + $fallback_image["#$key"] = $variables[$key]; + } + } + $output[] = drupal_render($fallback_image); + $output[] = ''; + return SafeMarkup::set(implode("\n", $output)); +} +/** + * Helper function for theme_responsive_image(). + * + * Builds an array of render arrays that will output tags to be used in + * a tag. In other words, this function provides the contents for a + * tag. + * + * In a responsive image mapping, each breakpoint has a mapping defined for each + * of its multipliers. A mapping can be either of two types: 'sizes' (meaning + * it will output a tag with the 'sizes' attribute) or 'image_style' + * (meaning it will output a tag based on the selected image style for + * this breakpoint and multiplier). When all the images in the 'srcset' + * attribute of a tag have the same MIME type, the source tag will get + * a 'mime-type' attribute as well. This way we can gain some front-end + * performance because browsers can select which image ( tag) to load + * based on the MIME types they support (which, for instance, can be beneficial + * for browsers supporting WebP). + * For example: + * A source tag can contain multiple images: + * @code + * + * @endcode + * In the above example we can add the mime-type attribute ('image/jpeg') since + * all images in the 'srcset' attribute of the tag have the same MIME + * type. + * If a source tag were to look like this: + * @code + * + * @endcode + * We can't add the 'mime-type' attribute ('image/jpeg' vs 'image/webp'). So in + * order to add the 'mime-type' attribute to the tag all images in the + * 'srcset' attribute of the tag need to be of the same MIME type. This + * way, a tag could look like this: + * @code + * + * + * + * + * + * @endcode + * This way a browser can decide which tag is preferred based on the + * MIME type. In other words, the MIME types of all images in one tag + * need to be the same in order to set the 'mime-type' attribute but not all + * MIME types within the tag need to be the same. + * + * For mappings of the type 'sizes', a width descriptor is added to each source. + * For example: + * @code + * + * @endcode + * The width descriptor here is "100w". This way the browser knows this image is + * 100px wide without having to load it. According to the spec, a multiplier can + * not be present if a width descriptor is. + * For example: + * Valid: + * @code + * + * @endcode + * Invalid: + * @code + * + * @endcode + * + * The data structure we use to store the mapping information is a + * multidimensional array. The first level is the breakpoint, the second level + * is the multiplier. Since the specs do not allow width descriptors + * and multipliers combined inside one 'srcset' attribute, we either have to use + * something like + * @code + * + * @endcode + * to support multipliers or + * @code + * + * @endcode + * to support the 'sizes' attribute. + * + * In theory people could add a mapping for the same breakpoint (but different + * multiplier) so the array contains an entry for breakpointA.1x and + * breakpointA.2x. If we would output those we will end up with something like + * @code + * + * @endcode + * which is illegal. So the solution is to merge both arrays into one and + * disregard the multiplier. Which, in this case, would output + * @code + * + * @endcode + * See http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#image-candidate-string + * for further information. + * + * @param ImageInterface $image + * The image to build the tags for. + * @param array $variables + * An array with the following keys: + * - mapping_id: The \Drupal\responsive_image\Entity\ResponsiveImageMapping + * ID. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + */ +function responsive_image_build_source_tags(ImageInterface $image, $variables) { + $sources = array(); + $width = isset($variables['width']) && !empty($variables['width']) ? $variables['width'] : $image->getWidth(); + $height = isset($variables['height']) && !empty($variables['height']) ? $variables['height'] : $image->getHeight(); + $extension = pathinfo($image->getSource(), PATHINFO_EXTENSION); $responsive_image_mapping = ResponsiveImageMapping::load($variables['mapping_id']); // All breakpoints and multipliers. $breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup($responsive_image_mapping->getBreakpointGroup()); foreach ($responsive_image_mapping->getKeyedMappings() as $breakpoint_id => $multipliers) { if (isset($breakpoints[$breakpoint_id])) { $breakpoint = $breakpoints[$breakpoint_id]; - $new_sources = array(); - foreach ($multipliers as $multiplier => $image_style) { - $new_source = $variables; - $new_source['style_name'] = $image_style; - $new_source['#multiplier'] = $multiplier; - $new_sources[] = $new_source; - } + $sizes = array(); + $srcset = array(); + $derivative_mime_types = array(); + foreach ($multipliers as $multiplier => $mapping_definition) { + switch ($mapping_definition['image_mapping_type']) { + // Create a tag with the 'sizes' attribute. + case 'sizes': + // Loop through the image styles for this breakpoint and multiplier. + foreach ($mapping_definition['sizes_image_styles'] as $image_style_name) { + // Get the dimensions. + $dimensions = responsive_image_get_image_dimensions($image_style_name, array('width' => $width, 'height' => $height)); + // Get MIME type. + $derivative_mime_type = responsive_image_get_mime_type($image_style_name, $extension); + $derivative_mime_types[] = $derivative_mime_type; - // Only one image, use src. - if (count($new_sources) == 1) { - $sources[] = array( - 'src' => _responsive_image_image_style_url($new_sources[0]['style_name'], $new_sources[0]['uri']), - 'dimensions' => responsive_image_get_image_dimensions($new_sources[0]), - 'media' => $breakpoint->getMediaQuery(), - ); - } - else { - // Multiple images, use srcset. - $srcset = array(); - foreach ($new_sources as $new_source) { - $srcset[] = _responsive_image_image_style_url($new_source['style_name'], $new_source['uri']) . ' ' . $new_source['#multiplier']; + // Add the image source with its width descriptor. When a width + // descriptor is used in a srcset, we can't add a multiplier to + // it. Because of this, the image styles for all multipliers of + // this breakpoint should be merged into one srcset and the sizes + // attribute should be merged as well. + $srcset[floatval($dimensions['width'])] = array( + 'uri' => _responsive_image_image_style_url($image_style_name, $image->getSource()), + 'width' => $dimensions['width'] . 'w', + ); + $sizes = array_merge(explode(',', $mapping_definition['sizes']), $sizes); + } + break; + + case 'image_style': + // Get MIME type. + $derivative_mime_type = responsive_image_get_mime_type($mapping_definition['image_style'], $extension); + $derivative_mime_types[] = $derivative_mime_type; + // Add the image source with its multiplier. + $srcset[floatval(Unicode::substr($multiplier, 0, -1))] = array( + 'uri' => _responsive_image_image_style_url($mapping_definition['image_style'], $image->getSource()), + 'multiplier' => $multiplier, + ); + break; } - $sources[] = array( - 'srcset' => implode(', ', $srcset), - 'dimensions' => responsive_image_get_image_dimensions($new_sources[0]), - 'media' => $breakpoint->getMediaQuery(), - ); } - } - } - - if (!empty($sources)) { - $output = array(); - $output[] = ''; - - // Add source tags to the output. - foreach ($sources as $source) { - $responsive_image_source = array( + ksort($srcset); + $sources[] = array( '#theme' => 'responsive_image_source', - '#src' => $source['src'], - '#dimensions' => $source['dimensions'], + '#srcset' => array_map('unserialize', array_unique(array_map('serialize', $srcset))), + '#media' => $breakpoint->getMediaQuery(), + // Only set a MIME type if it's the same for all derivative images. + '#mime_type' => count(array_unique($derivative_mime_types)) == 1 ? $derivative_mime_types[0] : '', + '#sizes' => implode(',', array_unique($sizes)), ); - if (isset($source['media'])) { - $responsive_image_source['#media'] = $source['media']; - } - if (isset($source['srcset'])) { - $responsive_image_source['#srcset'] = $source['srcset']; - } - $output[] = drupal_render($responsive_image_source); } - - $output[] = ''; - return SafeMarkup::set(implode("\n", $output)); } + return $sources; + } /** - * Returns HTML for a source tag. + * Prepares variables for tag templates. + * + * This template outputs a tag to be used in a tag. + * + * Default template: responsive-image-source.html.twig. * * @param array $variables * An associative array containing: * - media: The media query to use. - * - srcset: The srcset containing the the path of the image file or a full - * URL and optionally multipliers. - * - src: Either the path of the image file (relative to base_path()) or a - * full URL. - * - dimensions: The width and height of the image (if known). - * - * @ingroup themeable + * - srcset: Array of multiple URIs and sizes/multipliers. + * - mime_type: The MIME type of the image (if known). + * - sizes: The sizes attribute for the source element. */ -function theme_responsive_image_source($variables) { - $output = array(); - if (isset($variables['media']) && !empty($variables['media'])) { - if (!isset($variables['srcset'])) { - $output[] = ''; - $output[] = ''; +function template_preprocess_responsive_image_source(&$variables) { + $srcset = array(); + foreach ($variables['srcset'] as $src) { + // URI is mandatory. + if (!isset($src['uri'])) { + throw new \LogicException('Key \'uri\' is required for each source in a srcset.'); + } + $source = file_create_url($src['uri']); + if (isset($src['width']) && !empty($src['width'])) { + $source .= ' ' . $src['width']; } - elseif (!isset($variables['src'])) { - $output[] = ''; - $output[] = ''; + elseif (isset($src['multiplier']) && !empty($src['multiplier'])) { + $source .= ' ' . $src['multiplier']; } + $srcset[] = $source; } - else { - $output[] = ''; - $output[] = ''; + $variables['attributes']['srcset'] = implode(', ', $srcset); + if (isset($variables['media']) && !empty($variables['media'])) { + $variables['attributes']['media'] = $variables['media']; + } + if (isset($variables['mime_type']) && !empty($variables['mime_type'])) { + $variables['attributes']['type'] = $variables['mime_type']; + } + if (isset($variables['sizes']) && !empty($variables['sizes'])) { + $variables['attributes']['sizes'] = $variables['sizes']; } - return implode("\n", $output); } /** * Determines the dimensions of an image. * - * @param $variables + * @param string $image_style_name + * The name of the style to be used to alter the original image. + * @param array $dimensions * An associative array containing: - * - style_name: The name of the style to be used to alter the original image. * - width: The width of the source image (if known). * - height: The height of the source image (if known). * @@ -304,21 +446,16 @@ function theme_responsive_image_source($variables) { * Dimensions to be modified - an array with components width and height, in * pixels. */ -function responsive_image_get_image_dimensions($variables) { +function responsive_image_get_image_dimensions($image_style_name, array $dimensions) { // Determine the dimensions of the styled image. - $dimensions = array( - 'width' => $variables['width'], - 'height' => $variables['height'], - ); - - if ($variables['style_name'] == RESPONSIVE_IMAGE_EMPTY_IMAGE) { + if ($image_style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { $dimensions = array( 'width' => 1, 'height' => 1, ); } else { - $entity = entity_load('image_style', $variables['style_name']); + $entity = ImageStyle::load($image_style_name); if ($entity instanceof Drupal\image\Entity\ImageStyle) { $entity->transformDimensions($dimensions); } @@ -328,17 +465,38 @@ function responsive_image_get_image_dimensions($variables) { } /** + * Determines the MIME type of an image. + * + * @param string $image_style_name + * The image style that will be applied to the image. + * @param string $extension + * The original extension of the image (without the leading dot). + * + * @return string + * The MIME type of the image after the image style is applied. + */ +function responsive_image_get_mime_type($image_style_name, $extension) { + if ($image_style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { + return 'image/gif'; + } + // The MIME type guesser needs a full path, not just an extension, but the + // file doesn't have to exist. + $fake_path = 'responsive_image.' . ImageStyle::load($image_style_name)->getDerivativeExtension($extension); + return Drupal::service('file.mime_type.guesser.extension')->guess($fake_path); +} + +/** * Wrapper around image_style_url() so we can return an empty image. */ function _responsive_image_image_style_url($style_name, $path) { if ($style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { // The smallest data URI for a 1px square transparent GIF image. - return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever + return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; } - $entity = entity_load('image_style', $style_name); + $entity = ImageStyle::load($style_name); if ($entity instanceof Drupal\image\Entity\ImageStyle) { return $entity->buildUrl($path); } return file_create_url($path); } - diff --git a/core/modules/responsive_image/responsive_image.routing.yml b/core/modules/responsive_image/responsive_image.routing.yml index 09b14b5..31b9ee7 100644 --- a/core/modules/responsive_image/responsive_image.routing.yml +++ b/core/modules/responsive_image/responsive_image.routing.yml @@ -30,7 +30,7 @@ entity.responsive_image_mapping.duplicate_form: requirements: _permission: 'administer responsive images' -responsive_image.mapping_action_confirm: +entity.responsive_image_mapping.delete_form: path: '/admin/config/media/responsive-image-mapping/{responsive_image_mapping}/delete' defaults: _entity_form: 'responsive_image_mapping.delete' diff --git a/core/modules/responsive_image/src/Entity/ResponsiveImageMapping.php b/core/modules/responsive_image/src/Entity/ResponsiveImageMapping.php index 3e8440e..9c374cc 100644 --- a/core/modules/responsive_image/src/Entity/ResponsiveImageMapping.php +++ b/core/modules/responsive_image/src/Entity/ResponsiveImageMapping.php @@ -34,7 +34,8 @@ * }, * links = { * "edit-form" = "entity.responsive_image_mapping.edit_form", - * "duplicate-form" = "entity.responsive_image_mapping.duplicate_form" + * "duplicate-form" = "entity.responsive_image_mapping.duplicate_form", + * "delete-form" = "entity.responsive_image_mapping.delete_form" * } * ) */ @@ -88,18 +89,27 @@ public function __construct(array $values, $entity_type_id = 'responsive_image_m /** * {@inheritdoc} */ - public function addMapping($breakpoint_id, $multiplier, $image_style) { + public function addMapping($breakpoint_id, $multiplier, array $mapping_definition) { + $defaults = array( + 'image_mapping_type' => 'image_style', + 'image_style' => '', + 'sizes' => '', + 'sizes_image_styles' => array(), + ); + // If there is an existing mapping, overwrite it. foreach ($this->mappings as &$mapping) { if ($mapping['breakpoint_id'] === $breakpoint_id && $mapping['multiplier'] === $multiplier) { - $mapping['image_style'] = $image_style; + $mapping = array( + 'breakpoint_id' => $breakpoint_id, + 'multiplier' => $multiplier, + ) + $mapping_definition + $defaults; return $this; } } $this->mappings[] = array( 'breakpoint_id' => $breakpoint_id, 'multiplier' => $multiplier, - 'image_style' => $image_style, - ); + ) + $mapping_definition + $defaults; $this->keyedMappings = NULL; return $this; } @@ -108,7 +118,8 @@ public function addMapping($breakpoint_id, $multiplier, $image_style) { * {@inheritdoc} */ public function hasMappings() { - return !empty($this->mappings); + $mappings = $this->getKeyedMappings(); + return !empty($mappings); } /** @@ -118,7 +129,11 @@ public function getKeyedMappings() { if (!$this->keyedMappings) { $this->keyedMappings = array(); foreach($this->mappings as $mapping) { - $this->keyedMappings[$mapping['breakpoint_id']][$mapping['multiplier']] = $mapping['image_style']; + if (!$this->isEmptyMappingDefinition($mapping)) { + // Only return the selected image styles. + $mapping['sizes_image_styles'] = array_filter($mapping['sizes_image_styles']); + $this->keyedMappings[$mapping['breakpoint_id']][$mapping['multiplier']] = $mapping; + } } } return $this->keyedMappings; @@ -127,16 +142,6 @@ public function getKeyedMappings() { /** * {@inheritdoc} */ - public function getImageStyle($breakpoint_id, $multiplier) { - $map = $this->getKeyedMappings(); - if (isset($map[$breakpoint_id][$multiplier])) { - return $map[$breakpoint_id][$multiplier]; - } - } - - /** - * {@inheritdoc} - */ public function getMappings() { return $this->get('mappings'); } @@ -181,4 +186,38 @@ public function calculateDependencies() { return $this->dependencies; } + /** + * {@inheritdoc} + */ + public static function isEmptyMappingDefinition(array $mapping_definition) { + if (!empty($mapping_definition)) { + switch ($mapping_definition['image_mapping_type']) { + case 'sizes': + // The mapping definition must have a sizes attribute defined and one + // or more image styles selected. + if ($mapping_definition['sizes'] && array_filter($mapping_definition['sizes_image_styles'])) { + return FALSE; + } + break; + case 'image_style': + // The mapping definition must have an image style selected. + if ($mapping_definition['image_style']) { + return FALSE; + } + break; + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getMappingDefinition($breakpoint_id, $multiplier) { + $map = $this->getKeyedMappings(); + if (isset($map[$breakpoint_id][$multiplier])) { + return $map[$breakpoint_id][$multiplier]; + } + } + } diff --git a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php index a1308e9..91a0444 100644 --- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php +++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php @@ -15,6 +15,8 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\responsive_image\Entity\ResponsiveImageMapping; +use Drupal\image\Entity\ImageStyle; /** * Plugin for responsive image formatter. @@ -175,7 +177,7 @@ public function viewElements(FieldItemListInterface $items) { $elements = array(); // Check if the formatter involves a link. if ($this->getSetting('image_link') == 'content') { - $uri = $items->getEntity()->urlInfo(); + $uri = $items->getEntity()->urlInfo()->toArray(); // @todo Remove when theme_responsive_image_formatter() has support for route name. $uri['path'] = $items->getEntity()->getSystemPath(); } @@ -193,26 +195,44 @@ public function viewElements(FieldItemListInterface $items) { // Collect cache tags to be added for each item in the field. $responsive_image_mapping = $this->responsiveImageMappingStorage->load($this->getSetting('responsive_image_mapping')); $image_styles_to_load = array(); - if ($fallback_image_style) { - $image_styles_to_load[] = $fallback_image_style; - } $cache_tags = []; if ($responsive_image_mapping) { $cache_tags = Cache::mergeTags($cache_tags, $responsive_image_mapping->getCacheTags()); foreach ($responsive_image_mapping->getMappings() as $mapping) { - // First mapping found is used as fallback. - if (empty($fallback_image_style)) { - $fallback_image_style = $mapping['image_style']; + // Only image styles of non-empty mappings should be loaded. + if (!$responsive_image_mapping::isEmptyMappingDefinition($mapping)) { + $mapping['sizes_image_styles'] = array_filter($mapping['sizes_image_styles']); + if ($mapping['image_mapping_type'] == 'image_style') { + // This mapping has one image style, add it. + $image_styles_to_load[] = $mapping['image_style']; + } + else { + // This mapping has multiple image styles, merge them. + $image_styles_to_load = array_merge($image_styles_to_load, $mapping['sizes_image_styles']); + } } - $image_styles_to_load[] = $mapping['image_style']; } } - $image_styles = entity_load_multiple('image_style', $image_styles_to_load); + + // If there is a fallback image style, add it to the image styles to load. + if ($fallback_image_style) { + $image_styles_to_load[] = $fallback_image_style; + } + else { + // The element uses the first matching breakpoint (see + // http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#update-the-source-set + // points 2 and 3). Meaning the breakpoints are sorted from large to + // small. With mobile-first in mind, the fallback image should be the one + // selected for the smallest screen. + $fallback_image_style = end($image_styles_to_load); + } + $image_styles = ImageStyle::loadMultiple($image_styles_to_load); foreach ($image_styles as $image_style) { $cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags()); } foreach ($items as $delta => $item) { + // Link the element to the original file. if (isset($link_file)) { $uri = array( 'path' => file_create_url($item->entity->getFileUri()), diff --git a/core/modules/responsive_image/src/ResponsiveImageMappingForm.php b/core/modules/responsive_image/src/ResponsiveImageMappingForm.php index 0ad61ca..186d6fe 100644 --- a/core/modules/responsive_image/src/ResponsiveImageMappingForm.php +++ b/core/modules/responsive_image/src/ResponsiveImageMappingForm.php @@ -105,10 +105,18 @@ public function form(array $form, FormStateInterface $form_state) { foreach ($breakpoint->getMultipliers() as $multiplier) { $label = $multiplier . ' ' . $breakpoint->getLabel() . ' [' . $breakpoint->getMediaQuery() . ']'; $form['keyed_mappings'][$breakpoint_id][$multiplier] = array( + '#type' => 'container', + ); + $mapping_definition = $responsive_image_mapping->getMappingDefinition($breakpoint_id, $multiplier); + $form['keyed_mappings'][$breakpoint_id][$multiplier]['image_mapping_type'] = array( + '#type' => 'value', + '#value' => 'image_style', + ); + $form['keyed_mappings'][$breakpoint_id][$multiplier]['image_style'] = array( '#type' => 'select', '#title' => $label, '#options' => $image_styles, - '#default_value' => $responsive_image_mapping->getImageStyle($breakpoint_id, $multiplier), + '#default_value' => isset($mapping_definition['image_style']) ? $mapping_definition['image_style'] : array(), '#description' => $this->t('Select an image style for this breakpoint.'), ); } @@ -143,8 +151,8 @@ public function save(array $form, FormStateInterface $form_state) { $responsive_image_mapping->removeMappings(); if ($form_state->hasValue('keyed_mappings')) { foreach ($form_state->getValue('keyed_mappings') as $breakpoint_id => $multipliers) { - foreach ($multipliers as $multiplier => $image_style) { - $responsive_image_mapping->addMapping($breakpoint_id, $multiplier, $image_style); + foreach ($multipliers as $multiplier => $mapping) { + $responsive_image_mapping->addMapping($breakpoint_id, $multiplier, $mapping); } } } diff --git a/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php b/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php index fd9b28b..312fc66 100644 --- a/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php +++ b/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php @@ -27,7 +27,16 @@ public function hasMappings(); * * @return array[] * The responsive image mappings. Keyed by breakpoint ID then multiplier. - * The value is the image style ID. + * The value is the mapping definition array with following keys: + * - image_mapping_type: Either 'image_style' or 'sizes'. + * - image_style: If image_mapping_type is 'image_style', the image style + * ID. + * - sizes: If image_mapping_type is 'sizes', the value for the 'sizes' + * attribute. + * - sizes_image_styles: The image styles to use for the 'srcset' + * attribute. + * - breakpoint_id: The breakpoint ID for this mapping. + * - multiplier: The multiplier for this mapping. */ public function getKeyedMappings(); @@ -39,7 +48,10 @@ public function getKeyedMappings(); * contains the following keys: * - breakpoint_id * - multiplier + * - image_mapping_type * - image_style + * - sizes + * - sizes_image_styles */ public function getMappings(); @@ -62,17 +74,38 @@ public function setBreakpointGroup($breakpoint_group); public function getBreakpointGroup(); /** - * Gets the image style ID for a breakpoint ID and multiplier. + * Gets the mapping definition for a breakpoint ID and multiplier. * * @param string $breakpoint_id * The breakpoint ID. * @param string $multiplier * The multiplier. * - * @return string|null - * The image style ID. Null if the mapping does not exist. + * @return array|null + * The mapping definition. NULL if the mapping does not exist. + * The mapping definition has following keys: + * - image_mapping_type: Either 'image_style' or 'sizes'. + * - image_style: If image_mapping_type is 'image_style', the image style + * ID. + * - sizes: If image_mapping_type is 'sizes', the value for the 'sizes' + * attribute. + * - sizes_image_styles: The image styles to use for the 'srcset' + * attribute. + * - breakpoint_id: The breakpoint ID for this mapping definition. + * - multiplier: The multiplier for this mapping definition. */ - public function getImageStyle($breakpoint_id, $multiplier); + public function getMappingDefinition($breakpoint_id, $multiplier); + + /** + * Checks if there is at least one mapping definition defined. + * + * @param array $mapping_definition + * The mapping definition array. + * + * @return bool + * Whether the mapping definition is empty. + */ + public static function isEmptyMappingDefinition(array $mapping_definition); /** * Adds a mapping to the responsive image configuration entity. @@ -81,12 +114,12 @@ public function getImageStyle($breakpoint_id, $multiplier); * The breakpoint ID. * @param string $multiplier * The multiplier. - * @param string $image_style - * The image style ID. + * @param string $mapping_definition + * The mapping definition array. * * @return $this */ - public function addMapping($breakpoint_id, $multiplier, $image_style); + public function addMapping($breakpoint_id, $multiplier, array $mapping_definition); /** * Removes all mappings from the responsive image configuration entity. diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php index 7ddc0aa..a45caf7 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php @@ -69,30 +69,42 @@ public function testResponsiveImageAdmin() { $this->assertFieldByName('label', 'Mapping One'); $this->assertFieldByName('breakpointGroup', 'responsive_image_test_module'); - // Check if the dropdowns are present for the mappings. - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.mobile][1x]', ''); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.mobile][2x]', ''); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.narrow][1x]', ''); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.narrow][2x]', ''); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.wide][1x]', ''); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.wide][2x]', ''); + $cases = array( + array('mobile', '1x'), + array('mobile', '2x'), + array('narrow', '1x'), + array('narrow', '2x'), + array('wide', '1x'), + array('wide', '2x'), + ); + + foreach ($cases as $case) { + // Check if the radio buttons are present. + $this->assertFieldByName('keyed_mappings[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_style]', ''); + } // Save mappings for 1x variant only. $edit = array( 'label' => 'Mapping One', 'breakpointGroup' => 'responsive_image_test_module', - 'keyed_mappings[responsive_image_test_module.mobile][1x]' => 'thumbnail', - 'keyed_mappings[responsive_image_test_module.narrow][1x]' => 'medium', - 'keyed_mappings[responsive_image_test_module.wide][1x]' => 'large', + 'keyed_mappings[responsive_image_test_module.mobile][1x][image_style]' => 'thumbnail', + 'keyed_mappings[responsive_image_test_module.narrow][1x][image_style]' => 'medium', + 'keyed_mappings[responsive_image_test_module.wide][1x][image_style]' => 'large', ); $this->drupalPostForm('admin/config/media/responsive-image-mapping/mapping_one', $edit, t('Save')); $this->drupalGet('admin/config/media/responsive-image-mapping/mapping_one'); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.mobile][1x]', 'thumbnail'); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.mobile][2x]', ''); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.narrow][1x]', 'medium'); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.narrow][2x]', ''); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.wide][1x]', 'large'); - $this->assertFieldByName('keyed_mappings[responsive_image_test_module.wide][2x]', ''); + + // Check the mapping for multipliers 1x and 2x for the mobile breakpoint. + $this->assertFieldByName('keyed_mappings[responsive_image_test_module.mobile][1x][image_style]', 'thumbnail'); + $this->assertFieldByName('keyed_mappings[responsive_image_test_module.mobile][2x][image_style]', ''); + + // Check the mapping for multipliers 1x and 2x for the narrow breakpoint. + $this->assertFieldByName('keyed_mappings[responsive_image_test_module.narrow][1x][image_style]', 'medium'); + $this->assertFieldByName('keyed_mappings[responsive_image_test_module.narrow][2x][image_style]', ''); + + // Check the mapping for multipliers 1x and 2x for the wide breakpoint. + $this->assertFieldByName('keyed_mappings[responsive_image_test_module.wide][1x][image_style]', 'large'); + $this->assertFieldByName('keyed_mappings[responsive_image_test_module.wide][2x][image_style]', ''); // Delete the mapping. $this->drupalGet('admin/config/media/responsive-image-mapping/mapping_one/delete'); diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php index 41e4f1c..cd603ab 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php @@ -9,6 +9,9 @@ use Drupal\Component\Utility\Unicode; use Drupal\image\Tests\ImageFieldTestBase; +use Drupal\image\Entity\ImageStyle; +use Drupal\node\Entity\Node; +use Drupal\file\Entity\File; /** * Tests responsive image display formatter. @@ -63,7 +66,7 @@ protected function setUp() { } /** - * Test responsive image formatters on node display for public files. + * Tests responsive image formatters on node display for public files. */ public function testResponsiveImageFieldFormattersPublic() { $this->addTestMappings(); @@ -71,7 +74,7 @@ public function testResponsiveImageFieldFormattersPublic() { } /** - * Test responsive image formatters on node display for private files. + * Tests responsive image formatters on node display for private files. */ public function testResponsiveImageFieldFormattersPrivate() { $this->addTestMappings(); @@ -97,16 +100,49 @@ public function testResponsiveImageFieldFormattersEmptyStyle() { protected function addTestMappings($empty_styles = FALSE) { if ($empty_styles) { $this->responsiveImgMapping - ->addMapping('responsive_image_test_module.mobile', '1x', '') - ->addMapping('responsive_image_test_module.narrow', '1x', '') - ->addMapping('responsive_image_test_module.wide', '1x', '') + ->addMapping('responsive_image_test_module.mobile', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => '', + 'sizes' => '', + 'sizes_image_styles' => array(), + )) + ->addMapping('responsive_image_test_module.narrow', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width: 700px) 700px, 100vw', + 'sizes_image_styles' => array(), + )) + ->addMapping('responsive_image_test_module.wide', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => '', + 'sizes' => '', + 'sizes_image_styles' => array(), + )) ->save(); } else { $this->responsiveImgMapping - ->addMapping('responsive_image_test_module.mobile', '1x', 'thumbnail') - ->addMapping('responsive_image_test_module.narrow', '1x', 'medium') - ->addMapping('responsive_image_test_module.wide', '1x', 'large') + ->addMapping('responsive_image_test_module.mobile', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => RESPONSIVE_IMAGE_EMPTY_IMAGE, + 'sizes' => '', + 'sizes_image_styles' => array(), + )) + ->addMapping('responsive_image_test_module.narrow', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width: 700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + 'medium' => 'medium', + ), + )) + ->addMapping('responsive_image_test_module.wide', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + )) ->save(); } } @@ -133,7 +169,7 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = $node = $node_storage->load($nid); // Test that the default formatter is being used. - $image_uri = file_load($node->{$field_name}->target_id)->getFileUri(); + $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); $image = array( '#theme' => 'image', '#uri' => $image_uri, @@ -191,42 +227,115 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = $display->setComponent($field_name, $display_options) ->save(); + // Create a derivative so at least one MIME type will be known. + $large_style = ImageStyle::load('large'); + $large_style->createDerivative($image_uri, $large_style->buildUri($image_uri)); + // Output should contain all image styles and all breakpoints. $this->drupalGet('node/' . $nid); if (!$empty_styles) { - $this->assertRaw('/styles/thumbnail/'); $this->assertRaw('/styles/medium/'); + // Make sure the IE9 workaround is present. + $this->assertRaw(''); + $this->assertRaw(''); + $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); + $this->assertRaw('/styles/medium/'); + $this->assertRaw('media="(min-width: 0px)"'); + $this->assertRaw('media="(min-width: 560px)"'); + $this->assertRaw('sizes="(min-width: 700px) 700px, 100vw"'); + $this->assertPattern('/media="\(min-width: 560px\)".+?sizes="\(min-width: 700px\) 700px, 100vw"/'); + $this->assertRaw('media="(min-width: 851px)"'); } $this->assertRaw('/styles/large/'); - $this->assertRaw('media="(min-width: 0px)"'); - $this->assertRaw('media="(min-width: 560px)"'); - $this->assertRaw('media="(min-width: 851px)"'); $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags')); $this->assertTrue(in_array('responsive_image_mapping:mapping_one', $cache_tags)); if (!$empty_styles) { - $this->assertTrue(in_array('image_style:thumbnail', $cache_tags)); $this->assertTrue(in_array('image_style:medium', $cache_tags)); + $this->assertRaw('type="image/png"'); } $this->assertTrue(in_array('image_style:large', $cache_tags)); // Test the fallback image style. - $large_style = entity_load('image_style', 'large'); + $image = \Drupal::service('image.factory')->get($image_uri); $fallback_image = array( - '#theme' => 'responsive_image_source', - '#src' => $large_style->buildUrl($image_uri), - '#dimensions' => array('width' => 40, 'height' => 20), + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => $large_style->buildUrl($image->getSource()), + ), + ), ); $default_output = drupal_render($fallback_image); - $this->assertRaw($default_output, 'Image style thumbnail formatter displaying correctly on full node view.'); + $this->assertRaw($default_output, 'Image style large formatter displaying correctly on full node view.'); if ($scheme == 'private') { // Log out and try to access the file. $this->drupalLogout(); $this->drupalGet($large_style->buildUrl($image_uri)); - $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.'); + $this->assertResponse('403', 'Access denied to image style large as anonymous user.'); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); } } + /** + * Tests responsive image formatters on node display linked to the file. + */ + public function testResponsiveImageFieldFormattersLinkToFile() { + $this->addTestMappings(); + $this->assertResponsiveImageFieldFormattersLink('file'); + } + + /** + * Tests responsive image formatters on node display linked to the node. + */ + public function testResponsiveImageFieldFormattersLinkToNode() { + $this->addTestMappings(); + $this->assertResponsiveImageFieldFormattersLink('node'); + } + + /** + * Tests responsive image formatters linked to the file or node. + */ + private function assertResponsiveImageFieldFormattersLink($link_type) { + $field_name = Unicode::strtolower($this->randomMachineName()); + $this->createImageField($field_name, 'article', array('uri_scheme' => 'public')); + // Create a new node with an image attached. + $test_image = current($this->drupalGetTestFiles('image')); + $nid = $this->uploadNodeImage($test_image, $field_name, 'article'); + $this->container->get('entity.manager')->getStorage('node')->resetCache(array($nid)); + $node = Node::load($nid); + + // Use the responsive image formatter linked to file formatter. + $display_options = array( + 'type' => 'responsive_image', + 'settings' => array( + 'image_link' => $link_type, + 'responsive_image_mapping' => 'mapping_one', + 'fallback_image_style' => 'large', + ), + ); + entity_get_display('node', 'article', 'default') + ->setComponent($field_name, $display_options) + ->save(); + + // Create a derivative so at least one MIME type will be known. + $large_style = ImageStyle::load('large'); + $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); + $large_style->createDerivative($image_uri, $large_style->buildUri($image_uri)); + + // Output should contain all image styles and all breakpoints. + $this->drupalGet('node/' . $nid); + switch ($link_type) { + case 'file': + // Make sure the link to the file is present. + $this->assertPattern('/assertPattern('/url() . '"(.*?)> tag. + * + * Available variables: + * - attributes: HTML attributes for the tag. + * + * @see template_preprocess() + * @see template_preprocess_responsive_image_source() + * + * @ingroup themeable + */ +#} + diff --git a/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php b/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php index df8a0ab..b4f2861 100644 --- a/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php +++ b/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php @@ -85,19 +85,73 @@ public function testCalculateDependencies() { public function testHasMappings() { $entity = new ResponsiveImageMapping(array()); $this->assertFalse($entity->hasMappings()); - $entity->addMapping('test_breakpoint', '1x', 'test_style'); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => '', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $this->assertFalse($entity->hasMappings()); + $entity->removeMappings(); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array(), + )); + $this->assertFalse($entity->hasMappings()); + $entity->removeMappings(); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + )); + $this->assertFalse($entity->hasMappings()); + $entity->removeMappings(); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $this->assertTrue($entity->hasMappings()); + $entity->removeMappings(); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + )); $this->assertTrue($entity->hasMappings()); } /** * @covers ::addMapping - * @covers ::getImageStyle + * @covers ::getMappingDefinition */ - public function testGetImageStyle() { + public function testGetMappingDefinition() { $entity = new ResponsiveImageMapping(array('')); - $entity->addMapping('test_breakpoint', '1x', 'test_style'); - $this->assertEquals('test_style', $entity->getImageStyle('test_breakpoint', '1x')); - $this->assertNull($entity->getImageStyle('test_unknown_breakpoint', '1x')); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $expected = array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + ); + $this->assertEquals($expected, $entity->getMappingDefinition('test_breakpoint', '1x')); + $this->assertNull($entity->getMappingDefinition('test_unknown_breakpoint', '1x')); } /** @@ -106,24 +160,76 @@ public function testGetImageStyle() { */ public function testGetKeyedMappings() { $entity = new ResponsiveImageMapping(array('')); - $entity->addMapping('test_breakpoint', '1x', 'test_style'); - $entity->addMapping('test_breakpoint', '2x', 'test_style2'); - $entity->addMapping('test_breakpoint2', '1x', 'test_style3'); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $entity->addMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + )); + $entity->addMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'thumbnail', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); $expected = array( 'test_breakpoint' => array( - '1x' => 'test_style', - '2x' => 'test_style2', + '1x' => array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + ), + '2x' => array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '2x', + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), ), 'test_breakpoint2' => array( - '1x' => 'test_style3', + '1x' => array( + 'breakpoint_id' => 'test_breakpoint2', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_style' => 'thumbnail', + 'sizes' => '', + 'sizes_image_styles' => array(), + ), ) ); $this->assertEquals($expected, $entity->getKeyedMappings()); // Add another mapping to ensure keyed mapping static cache is rebuilt. - $entity->addMapping('test_breakpoint2', '2x', 'test_style4'); - $expected['test_breakpoint2']['2x'] = 'test_style4'; + $entity->addMapping('test_breakpoint2', '2x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'medium', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $expected['test_breakpoint2']['2x'] = array( + 'breakpoint_id' => 'test_breakpoint2', + 'multiplier' => '2x', + 'image_mapping_type' => 'image_style', + 'image_style' => 'medium', + 'sizes' => '', + 'sizes_image_styles' => array(), + ); $this->assertEquals($expected, $entity->getKeyedMappings()); } @@ -133,25 +239,53 @@ public function testGetKeyedMappings() { */ public function testGetMappings() { $entity = new ResponsiveImageMapping(array('')); - $entity->addMapping('test_breakpoint', '1x', 'test_style'); - $entity->addMapping('test_breakpoint', '2x', 'test_style2'); - $entity->addMapping('test_breakpoint2', '1x', 'test_style3'); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $entity->addMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + )); + $entity->addMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'thumbnail', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); $expected = array( array( 'breakpoint_id' => 'test_breakpoint', 'multiplier' => '1x', - 'image_style' => 'test_style', + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), ), array( 'breakpoint_id' => 'test_breakpoint', 'multiplier' => '2x', - 'image_style' => 'test_style2', + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), ), array( 'breakpoint_id' => 'test_breakpoint2', 'multiplier' => '1x', - 'image_style' => 'test_style3', + 'image_mapping_type' => 'image_style', + 'image_style' => 'thumbnail', + 'sizes' => '', + 'sizes_image_styles' => array(), ), ); $this->assertEquals($expected, $entity->getMappings()); @@ -163,9 +297,26 @@ public function testGetMappings() { */ public function testRemoveMappings() { $entity = new ResponsiveImageMapping(array('')); - $entity->addMapping('test_breakpoint', '1x', 'test_style'); - $entity->addMapping('test_breakpoint', '2x', 'test_style2'); - $entity->addMapping('test_breakpoint2', '1x', 'test_style3'); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $entity->addMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + )); + $entity->addMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'thumbnail', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); $this->assertTrue($entity->hasMappings()); $entity->removeMappings(); @@ -180,9 +331,26 @@ public function testRemoveMappings() { */ public function testSetBreakpointGroup() { $entity = new ResponsiveImageMapping(array('breakpointGroup' => 'test_group')); - $entity->addMapping('test_breakpoint', '1x', 'test_style'); - $entity->addMapping('test_breakpoint', '2x', 'test_style2'); - $entity->addMapping('test_breakpoint2', '1x', 'test_style3'); + $entity->addMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); + $entity->addMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + )); + $entity->addMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_style' => 'thumbnail', + 'sizes' => '', + 'sizes_image_styles' => array(), + )); // Ensure that setting to same group does not remove mappings. $entity->setBreakpointGroup('test_group'); diff --git a/core/modules/toolbar/toolbar.breakpoints.yml b/core/modules/toolbar/toolbar.breakpoints.yml index a68784a..df9f61b 100644 --- a/core/modules/toolbar/toolbar.breakpoints.yml +++ b/core/modules/toolbar/toolbar.breakpoints.yml @@ -1,7 +1,7 @@ toolbar.narrow: label: narrow mediaQuery: 'only screen and (min-width: 16.5em)' - weight: 0 + weight: 2 multipliers: - 1x toolbar.standard: @@ -13,6 +13,6 @@ toolbar.standard: toolbar.wide: label: wide mediaQuery: 'only screen and (min-width: 61em)' - weight: 2 + weight: 0 multipliers: - 1x diff --git a/core/themes/bartik/bartik.breakpoints.yml b/core/themes/bartik/bartik.breakpoints.yml index 17b9fc8..8e420b0 100644 --- a/core/themes/bartik/bartik.breakpoints.yml +++ b/core/themes/bartik/bartik.breakpoints.yml @@ -1,7 +1,7 @@ bartik.mobile: label: mobile mediaQuery: '(min-width: 0px)' - weight: 0 + weight: 2 multipliers: - 1x bartik.narrow: @@ -13,6 +13,6 @@ bartik.narrow: bartik.wide: label: wide mediaQuery: 'all and (min-width: 851px)' - weight: 2 + weight: 0 multipliers: - 1x diff --git a/core/themes/seven/seven.breakpoints.yml b/core/themes/seven/seven.breakpoints.yml index 1b6bd2f..9cc1fda 100644 --- a/core/themes/seven/seven.breakpoints.yml +++ b/core/themes/seven/seven.breakpoints.yml @@ -1,12 +1,12 @@ seven.mobile: label: mobile mediaQuery: '(min-width: 0em)' - weight: 0 + weight: 1 multipliers: - 1x seven.wide: label: wide mediaQuery: 'screen and (min-width: 40em)' - weight: 1 + weight: 0 multipliers: - 1x diff --git a/core/themes/stark/stark.breakpoints.yml b/core/themes/stark/stark.breakpoints.yml index d1cdd9b..92d33b2 100644 --- a/core/themes/stark/stark.breakpoints.yml +++ b/core/themes/stark/stark.breakpoints.yml @@ -1,7 +1,7 @@ stark.mobile: label: mobile mediaQuery: '(min-width: 0px)' - weight: 0 + weight: 2 multipliers: - 1x stark.narrow: @@ -13,6 +13,6 @@ stark.narrow: stark.wide: label: wide mediaQuery: 'all and (min-width: 960px)' - weight: 2 + weight: 0 multipliers: - 1x