diff --git a/core/assets/vendor/picturefill/picturefill.js b/core/assets/vendor/picturefill/picturefill.js index 4d69569..b6e288e 100644 --- a/core/assets/vendor/picturefill/picturefill.js +++ b/core/assets/vendor/picturefill/picturefill.js @@ -1,126 +1,573 @@ -/*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 - 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 ) { + // 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 + pf.srcsetSupported = "srcset" in doc.createElement( "img" ); + pf.sizesSupported = w.HTMLImageElement.sizes; + + // 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 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 no length was specified, or it is 0 or negative, default to `100vw` (per the spec). + length = length && ( 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" ); + doc.documentElement.insertBefore( pf.lengthEl, doc.documentElement.firstChild ); + } + + // Positioning styles help prevent padding/margin/width on `html` from throwing calculations off. + pf.lengthEl.style.cssText = "position: absolute; left: 0; width: " + length + ";"; + + 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%;"; + } + + return pf.lengthEl.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 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 { + // 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[ url.length - 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--) { + if ( splitDescriptor[ i ] !== undefined ) { + 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 ) ) { + picImg.src = bestCandidate.url; + // currentSrc attribute and property to match + // http://picture.responsiveimages.org/#the-img-element + picImg.currentSrc = picImg.src; + } + }; + + 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; + + // 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, + // 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 ); + 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 ); + } + } + + 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 ); \ No newline at end of file diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 887b14f..be9dac7 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.1.0 license: name: MIT - url: https://github.com/scottjehl/picturefill/blob/master/LICENSE + url: https://github.com/scottjehl/picturefill/blob/2.1.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 aac9add..10f0388 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,9 @@ responsive_image.mappings.*: - type: mapping label: 'Mapping' mapping: + image_mapping_type: + type: string + label: 'Type' breakpoint_id: type: string label: 'Breakpoint ID' @@ -29,6 +32,15 @@ responsive_image.mappings.*: image_style: type: string label: 'Image style' + sizes: + type: string + label: 'Sizes' + sizes_image_styles: + type: sequence + label: 'Image styles' + 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 63c732a..244e904 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -8,6 +8,8 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Routing\RouteMatchInterface; use \Drupal\Core\Template\Attribute; +use Drupal\image\Entity\ImageStyle; +use Drupal\responsive_image\Entity\ResponsiveImageMapping; /** * The machine name for the empty image breakpoint image style option. @@ -91,10 +93,10 @@ 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, ), ), ); @@ -185,68 +187,94 @@ function theme_responsive_image($variables) { $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), - ); - $responsive_image_mapping = entity_load('responsive_image_mapping', $variables['mapping_id']); + $image = \Drupal::service('image.factory')->get($variables['uri']); + $mime_type = $image->getMimeType(); + $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 source 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' => $variables['width'], 'height' => $variables['height'])); + // Get MIME type. + $derivative_mime_type = responsive_image_get_mime_type($image_style_name, $mime_type); + $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 in to one srcset and the sizes + // attribute should be merged as well. + // http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#image-candidate-string + $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'], $mime_type); + $derivative_mime_types[] = $derivative_mime_type; + // Add the image source with its multiplier. + $srcset[floatval(drupal_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(), - ); } + ksort($srcset); + $sources[] = array( + '#theme' => 'responsive_image_source', + '#srcset' => array_map('unserialize', array_unique(array_map('serialize', $srcset))), + '#media' => $breakpoint->getMediaQuery(), + // Only set a MIME type if all derivative images have the same MIME type. + '#mime_type' => count(array_unique($derivative_mime_types)) == 1 ? $derivative_mime_types[0] : '', + '#sizes' => implode(',', array_unique($sizes)), + ); } } if (!empty($sources)) { $output = array(); $output[] = ''; + // Internet Explorer 9 doesn't recognise source elements that are wrapped in + // picture tags. See http://scottjehl.github.io/picturefill/#ie9 + $output[] = ''; + $output = array_merge($output, array_map('drupal_render', $sources)); + $output[] = ''; + // Output the fallback image. + $dimensions = responsive_image_get_image_dimensions($variables['style_name'], array('width' => $variables['width'], 'height' => $variables['height'])); - // Add source tags to the output. - foreach ($sources as $source) { - $responsive_image_source = array( - '#theme' => 'responsive_image_source', - '#src' => $source['src'], - '#dimensions' => $source['dimensions'], - ); - if (isset($source['media'])) { - $responsive_image_source['#media'] = $source['media']; - } - if (isset($source['srcset'])) { - $responsive_image_source['#srcset'] = $source['srcset']; + // Use srcset in the fallback image to avoid unnecessary preloading of + // images in older browsers. + $fallback_image = array( + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => _responsive_image_image_style_url($variables['style_name'], $image->getSource()), + 'width' => $dimensions['width'] . 'w', + ), + ), + ); + foreach (array('alt', 'title', 'attributes') as $key) { + if (isset($variables[$key])) { + $fallback_image["#$key"] = $variables[$key]; } - $output[] = drupal_render($responsive_image_source); } - + $output[] = drupal_render($fallback_image, TRUE); $output[] = ''; return SafeMarkup::set(implode("\n", $output)); } @@ -258,39 +286,45 @@ function theme_responsive_image($variables) { * @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). + * - 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. * * @ingroup themeable */ function theme_responsive_image_source($variables) { - $output = array(); - if (isset($variables['media']) && !empty($variables['media'])) { - if (!isset($variables['srcset'])) { - $output[] = ''; - $output[] = ''; + $srcset = array(); + foreach ($variables['srcset'] as $src) { + // URI is mandatory. + $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[] = ''; + $attributes['srcset'] = implode(', ', $srcset); + if (isset($variables['media']) && !empty($variables['media'])) { + $attributes['media'] = $variables['media']; + } + if (isset($variables['mime_type']) && !empty($variables['mime_type'])) { + $attributes['type'] = $variables['mime_type']; } - return implode("\n", $output); + if (isset($variables['sizes']) && !empty($variables['sizes'])) { + $attributes['sizes'] = $variables['sizes']; + } + return ''; } /** * Determines the dimensions of an image. * - * @param $variables + * @param $image_style_name + * The name of the style to be used to alter the original image. + * @param $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). * @@ -298,34 +332,49 @@ 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, $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_load('image_style', $variables['style_name'])->transformDimensions($dimensions); + ImageStyle::load($image_style_name)->transformDimensions($dimensions); } return $dimensions; } /** +* Determines the MIME type of an image. + * + * @param $image_style_name + * The image style that will be applied to the image. + * @param $mime_type + * The original MIME type of the image. + * + * @return string + * The MIME type of the image after the image style is applied. + */ +function responsive_image_get_mime_type($image_style_name, $mime_type) { + if ($image_style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { + return 'image/gif'; + } + ImageStyle::load($image_style_name)->getDerivativeMimeType($mime_type); + return $mime_type; +} + +/** * 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=='; } - return entity_load('image_style', $style_name)->buildUrl($path); + return ImageStyle::load($style_name)->buildUrl($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 68aa58b..0070a05 100644 --- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php +++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php @@ -11,6 +11,8 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase; +use Drupal\responsive_image\Entity\ResponsiveImageMapping; +use Drupal\image\Entity\ImageStyle; /** * Plugin for responsive image formatter. @@ -41,7 +43,7 @@ public static function defaultSettings() { */ public function settingsForm(array $form, FormStateInterface $form_state) { $responsive_image_options = array(); - $responsive_image_mappings = entity_load_multiple('responsive_image_mapping'); + $responsive_image_mappings = ResponsiveImageMapping::loadMultiple(); if ($responsive_image_mappings && !empty($responsive_image_mappings)) { foreach ($responsive_image_mappings as $machine_name => $responsive_image_mapping) { if ($responsive_image_mapping->hasMappings()) { @@ -88,7 +90,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { public function settingsSummary() { $summary = array(); - $responsive_image_mapping = entity_load('responsive_image_mapping', $this->getSetting('responsive_image_mapping')); + $responsive_image_mapping = ResponsiveImageMapping::load($this->getSetting('responsive_image_mapping')); if ($responsive_image_mapping) { $summary[] = t('Responsive image mapping: @responsive_image_mapping', array('@responsive_image_mapping' => $responsive_image_mapping->label())); @@ -140,23 +142,38 @@ public function viewElements(FieldItemListInterface $items) { } // Collect cache tags to be added for each item in the field. - $responsive_image_mapping = entity_load('responsive_image_mapping', $this->getSetting('responsive_image_mapping')); + $responsive_image_mapping = ResponsiveImageMapping::load($this->getSetting('responsive_image_mapping')); $image_styles_to_load = array(); - if ($fallback_image_style) { - $image_styles_to_load[] = $fallback_image_style; - } $all_cache_tags = array(); if ($responsive_image_mapping) { $all_cache_tags[] = $responsive_image_mapping->getCacheTag(); 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 { + // Picturefill uses the first matching breakpoint. 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) { $all_cache_tags[] = $image_style->getCacheTag(); } @@ -164,6 +181,7 @@ public function viewElements(FieldItemListInterface $items) { $cache_tags = NestedArray::mergeDeepArray($all_cache_tags); foreach ($items as $delta => $item) { + // Link the picture 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..da5a0bb 100644 --- a/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php +++ b/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php @@ -27,7 +27,13 @@ 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: One of 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 +45,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 +71,35 @@ 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: One of 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 getImageStyle($breakpoint_id, $multiplier); + public function getMappingDefinition($breakpoint_id, $multiplier); + + /** + * Checks if there is at least one mapping 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 +108,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/ResponsiveImageMappingListBuilder.php b/core/modules/responsive_image/src/ResponsiveImageMappingListBuilder.php index b12fb61..2b94127 100644 --- a/core/modules/responsive_image/src/ResponsiveImageMappingListBuilder.php +++ b/core/modules/responsive_image/src/ResponsiveImageMappingListBuilder.php @@ -38,10 +38,10 @@ public function buildRow(EntityInterface $entity) { */ public function getDefaultOperations(EntityInterface $entity) { $operations = parent::getDefaultOperations($entity); - $operations['duplicate'] = array( - 'title' => t('Duplicate'), - 'weight' => 15, - ) + $entity->urlInfo('duplicate-form')->toArray(); + $operations['duplicate'] = array( + 'title' => t('Duplicate'), + 'weight' => 15, + ) + $entity->urlInfo('duplicate-form')->toArray(); return $operations; } 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 44dab9b..5e58613 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php @@ -8,6 +8,7 @@ namespace Drupal\responsive_image\Tests; use Drupal\image\Tests\ImageFieldTestBase; +use Drupal\image\Entity\ImageStyle; /** * Tests responsive image display formatter. @@ -53,9 +54,27 @@ protected function setUp() { 'breakpointGroup' => 'responsive_image_test_module', )); $responsive_image_mapping - ->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(); } @@ -146,35 +165,50 @@ public function _testResponsiveImageFieldFormatters($scheme) { $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); - $this->assertRaw('/styles/thumbnail/'); + // 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('/styles/large/'); $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)"'); $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags')); $this->assertTrue(in_array('responsive_image_mapping:mapping_one', $cache_tags)); - $this->assertTrue(in_array('image_style:thumbnail', $cache_tags)); $this->assertTrue(in_array('image_style:medium', $cache_tags)); $this->assertTrue(in_array('image_style:large', $cache_tags)); + $this->assertRaw('type="image/png"'); // Test the fallback image style. - $large_style = entity_load('image_style', 'large'); + $image = \Drupal::service('image.factory')->get($image_uri); + $dimensions = array('width' => $image->getWidth(), 'height' => $image->getHeight()); + $large_style->transformDimensions($dimensions); $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()), + 'width' => $dimensions['width'] . 'w', + ), + ), ); $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.'); } diff --git a/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php b/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php index 99c08b8..83e9880 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