diff --git a/core/assets/vendor/picturefill/picturefill.js b/core/assets/vendor/picturefill/picturefill.js index 4d69569..66bb1a9 100644 --- a/core/assets/vendor/picturefill/picturefill.js +++ b/core/assets/vendor/picturefill/picturefill.js @@ -1,126 +1,506 @@ -/*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 ){ + 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 = new w.Image().srcset !== undefined; + + // 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, default to `100vw` (per the spec). + length = length && parseFloat( length ) > 0 ? 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 + ";"; + // Using offsetWidth to get width from CSS + 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 ); + }; + + /** + * 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.trim( srcset ).split( /,\s+/ ), + widthInCssPixels = sizes ? pf.findWidthFromSourceSize( sizes ) : "100%", + formattedCandidates = []; + + for ( var i = 0, len = candidates.length; i < len; i++ ) { + var candidate = candidates[ i ], + candidateArr = candidate.split( /\s+/ ), + sizeDescriptor = candidateArr[ 1 ], + resolution; + if ( sizeDescriptor && ( sizeDescriptor.slice( -1 ) === "w" || sizeDescriptor.slice( -1 ) === "x" ) ) { + sizeDescriptor = sizeDescriptor.slice( 0, -1 ); + } + if ( sizes ) { + // get the dpr by taking the length / width in css pixels + resolution = parseFloat( ( parseInt( sizeDescriptor, 10 ) / widthInCssPixels ) ); + } else { + // get the dpr by grabbing the value of Nx + resolution = sizeDescriptor ? parseFloat( sizeDescriptor, 10 ) : 1; + } + + var formattedCandidate = { + url: candidateArr[ 0 ], + resolution: resolution + }; + formattedCandidates.push( formattedCandidate ); + } + 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 l=0; l < length; l++ ) { + candidate = candidates[ l ]; + if ( candidate.resolution >= pf.getDpr() ) { + bestCandidate = candidate; + break; + } + } + + if ( !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 picture elements and, + * in browsers that don't natively support srcset, find all img elements + * with srcset attrs that don't have picture parents + */ + pf.getAllElements = function() { + var pictures = doc.getElementsByTagName( "picture" ), + elems = [], + imgs = doc.getElementsByTagName( "img" ); + + for ( var h = 0, len = pictures.length + imgs.length; h < len; h++ ) { + if ( h < pictures.length ){ + elems[ h ] = pictures[ h ]; + } + else { + var currImg = imgs[ h - pictures.length ]; + + if ( currImg.parentNode.nodeName.toUpperCase() !== "PICTURE" && + ( ( pf.srcsetSupported && currImg.getAttribute( "sizes" ) ) || + currImg.getAttribute( "srcset" ) !== null ) ) { + elems.push( currImg ); + } + } + } + return elems; + }; + + pf.getMatch = function( 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 an `img` element stops the search for `sources`. + // If no previous `source` matches, the `img` itself is evaluated later. + if( source.nodeName.toUpperCase() === "IMG" ) { + return match; + } + + // ignore non-`source` nodes + if( source.nodeName.toUpperCase() !== "SOURCE" ){ + continue; + } + + 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( options ) { + var elements, + element, + elemType, + firstMatch, + candidates, + picImg; + + options = options || {}; + elements = options.elements || pf.getAllElements(); + + // Loop through all elements + for ( var i=0, plen = elements.length; i < plen; i++ ) { + element = elements[ i ]; + elemType = element.nodeName.toUpperCase(); + firstMatch = undefined; + candidates = undefined; + picImg = 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 element is a picture element + if( elemType === "PICTURE" ){ + + // IE9 video workaround + pf.removeVideoShim( element ); + + // 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 ); + + // 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; + } + + // Find any existing img element in the picture element + picImg = element.getElementsByTagName( "img" )[ 0 ]; + } else { + // if it's an img element + firstMatch = undefined; + picImg = element; + } + + if( picImg ) { + + // expando for caching data on the img + if( !picImg[ pf.ns ] ){ + picImg[ pf.ns ] = {}; + } + + // Cache and remove `srcset` if present and we’re going to be doing `sizes`/`picture` polyfilling to it. + if( picImg.srcset && ( elemType === "PICTURE" || picImg.getAttribute( "sizes" ) ) ){ + pf.dodgeSrcset( picImg ); + } + + if ( firstMatch ) { + candidates = pf.processSourceSet( firstMatch ); + pf.applyBestCandidate( candidates, picImg ); + } else { + // No sources matched, so we’re down to processing the inner `img` as a source. + candidates = pf.processSourceSet( picImg ); + + if( picImg.srcset === undefined || ( picImg.getAttribute( "sizes" ) && picImg[ pf.ns ].srcset ) ) { + // Either `srcset` is completely unsupported, or we need to polyfill `sizes` functionality. + pf.applyBestCandidate( candidates, picImg ); + } // 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 + w.picturefill(); + if ( /^loaded|^i|^c/.test( doc.readyState ) ) { + clearInterval( intervalId ); + return; + } + }, 250 ); + if( w.addEventListener ){ + var resizeThrottle; + w.addEventListener( "resize", function() { + w.clearTimeout( resizeThrottle ); + resizeThrottle = w.setTimeout( function(){ + picturefill({ reevaluate: true }); + }, 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 === "object" && 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 ); diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 12c11e6..80e4527 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1276,9 +1276,31 @@ function template_preprocess_links(&$variables) { * - title: The title text is displayed when the image is hovered in some * popular browsers. * - attributes: Associative array of attributes to be placed in the img tag. + * - srcset: Array of multiple URI's and sizes/multipliers. */ function template_preprocess_image(&$variables) { - $variables['attributes']['src'] = file_create_url($variables['uri']); + if (isset($variables['uri']) && !empty($variables['uri'])) { + $variables['attributes']['src'] = file_create_url($variables['uri']); + } + + if (isset($variables['srcset']) && !empty($variables['srcset'])) { + $srcsets = array(); + foreach ($variables['srcset'] as $srcset) { + // URI is mandatory. + if (isset($srcset['uri']) && !empty($srcset['uri'])) { + $srcset['src'] = file_create_url($srcset['uri']); + $new_source = $srcset['src']; + if (isset($srcset['width']) && !empty($srcset['width'])) { + $new_source .= ' ' . $srcset['width']; + } + elseif (isset($srcset['multiplier']) && !empty($srcset['multiplier'])) { + $new_source .= ' ' . $srcset['multiplier']; + } + $srcsets[] = $new_source; + } + } + $variables['attributes']['srcset'] = implode(', ', $srcsets); + } foreach (array('width', 'height', 'alt', 'title') as $key) { if (isset($variables[$key])) { @@ -2545,7 +2567,7 @@ function drupal_common_theme() { // - http://dev.w3.org/html5/spec/Overview.html#alt // The title attribute is optional in all cases, so it is omitted by // default. - 'variables' => array('uri' => NULL, 'width' => NULL, 'height' => NULL, 'alt' => '', 'title' => NULL, 'attributes' => array()), + 'variables' => array('uri' => NULL, 'width' => NULL, 'height' => NULL, 'alt' => '', 'title' => NULL, 'attributes' => array(), 'srcset' => array()), 'template' => 'image', ), 'breadcrumb' => array( diff --git a/core/modules/breakpoint/config/install/breakpoint.breakpoint.module.breakpoint._none.yml b/core/modules/breakpoint/config/install/breakpoint.breakpoint.module.breakpoint._none.yml new file mode 100644 index 0000000..13d876c --- /dev/null +++ b/core/modules/breakpoint/config/install/breakpoint.breakpoint.module.breakpoint._none.yml @@ -0,0 +1,11 @@ +id: module.breakpoint._none +name: _none +label: None (empty media query) +mediaQuery: '' +source: breakpoint +sourceType: module +weight: 99 +multipliers: + 1x: 1x +status: true +langcode: en diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php index 714e44e..2298106 100644 --- a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php @@ -178,7 +178,11 @@ public function isValid() { if (preg_match('/[^0-9a-z_\-]/', $this->name)) { throw new InvalidBreakpointNameException(format_string("Invalid value '@name' for breakpoint name property. Breakpoint name property can only contain lowercase alphanumeric characters, underscores (_), and hyphens (-).", array('@name' => $this->name))); } - return $this::isValidMediaQuery($this->mediaQuery); + // Skip the empty media query provided by the breakpoint module. + if ($this->id != 'module.breakpoint._none') { + return $this::isValidMediaQuery($this->mediaQuery); + } + return TRUE; } /** diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php index c2426b7..c089a97 100644 --- a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php @@ -197,6 +197,7 @@ public function getBreakpoints() { } } } + uasort($this->breakpoints, array('\Drupal\breakpoint\Entity\Breakpoint', 'sort')); return $this->breakpoints; } diff --git a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.mobile.yml b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.mobile.yml index 8e5e3c4..ed9d2ee 100644 --- a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.mobile.yml +++ b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.mobile.yml @@ -4,7 +4,7 @@ label: mobile mediaQuery: '(min-width: 0px)' source: breakpoint_test_theme sourceType: theme -weight: 0 +weight: 3 multipliers: 1x: 1x status: true diff --git a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.narrow.yml b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.narrow.yml index a34b582..a4bd013 100644 --- a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.narrow.yml +++ b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.narrow.yml @@ -4,7 +4,7 @@ label: narrow mediaQuery: '(min-width: 560px)' source: breakpoint_test_theme sourceType: theme -weight: 1 +weight: 2 multipliers: 1x: 1x status: true diff --git a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.tv.yml b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.tv.yml index d594ff4..adb73d2 100644 --- a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.tv.yml +++ b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.tv.yml @@ -4,7 +4,7 @@ label: tv mediaQuery: 'only screen and (min-width: 3456px)' source: breakpoint_test_theme sourceType: theme -weight: 3 +weight: 0 multipliers: 1x: 1x status: true diff --git a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.wide.yml b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.wide.yml index 34e5f4f..a985f78 100644 --- a/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.wide.yml +++ b/core/modules/breakpoint/tests/themes/breakpoint_test_theme/config/install/breakpoint.breakpoint.theme.breakpoint_test_theme.wide.yml @@ -4,7 +4,7 @@ label: wide mediaQuery: '(min-width: 851px)' source: breakpoint_test_theme sourceType: theme -weight: 2 +weight: 1 multipliers: 1x: 1x status: true diff --git a/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php b/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php index 4eaa2ec..ac028a2 100644 --- a/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php +++ b/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php @@ -312,6 +312,15 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + public function transformMimeType(&$mime_type) { + foreach ($this->getEffects() as $effect) { + $effect->transformMimeType($mime_type); + } + } + + /** + * {@inheritdoc} + */ public function getPathToken($uri) { // Return the first 8 characters. return substr(Crypt::hmacBase64($this->id() . ':' . $uri, \Drupal::service('private_key')->get() . drupal_get_hash_salt()), 0, 8); diff --git a/core/modules/image/lib/Drupal/image/ImageEffectBase.php b/core/modules/image/lib/Drupal/image/ImageEffectBase.php index ea3208a..099eea0 100644 --- a/core/modules/image/lib/Drupal/image/ImageEffectBase.php +++ b/core/modules/image/lib/Drupal/image/ImageEffectBase.php @@ -47,6 +47,13 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + public function transformMimeType(&$mime_type) { + $mime_type = NULL; + } + + /** + * {@inheritdoc} + */ public function getSummary() { return array( '#markup' => '', diff --git a/core/modules/image/lib/Drupal/image/ImageEffectInterface.php b/core/modules/image/lib/Drupal/image/ImageEffectInterface.php index 66b7f4e..719f0b7 100644 --- a/core/modules/image/lib/Drupal/image/ImageEffectInterface.php +++ b/core/modules/image/lib/Drupal/image/ImageEffectInterface.php @@ -37,6 +37,14 @@ public function applyEffect(ImageInterface $image); public function transformDimensions(array &$dimensions); /** + * Determines the mime type of the styled image. + * + * @param string $mime_type + * Mime type to be modified. + */ + public function transformMimeType(&$mime_type); + + /** * Returns a render array summarizing the configuration of the image effect. * * @return array diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php index 4d899c6..4db895c 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/DesaturateImageEffect.php @@ -30,6 +30,12 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + public function transformMimeType(&$mime_type) { + } + + /** + * {@inheritdoc} + */ public function applyEffect(ImageInterface $image) { if (!$image->desaturate()) { watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->getToolkitId(), '%path' => $image->getSource(), '%mimetype' => $image->getMimeType(), '%dimensions' => $image->getWidth() . 'x' . $image->getHeight()), WATCHDOG_ERROR); diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php index da9b176..81857f2 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ResizeImageEffect.php @@ -45,6 +45,12 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + public function transformMimeType(&$mime_type) { + } + + /** + * {@inheritdoc} + */ public function getSummary() { return array( '#theme' => 'image_resize_summary', diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php index b0e6bf3..2fc516a 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/RotateImageEffect.php @@ -73,6 +73,12 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + public function transformMimeType(&$mime_type) { + } + + /** + * {@inheritdoc} + */ public function getSummary() { return array( '#theme' => 'image_rotate_summary', diff --git a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php index 4316bd0..88f5fee 100644 --- a/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php +++ b/core/modules/image/lib/Drupal/image/Plugin/ImageEffect/ScaleImageEffect.php @@ -44,6 +44,12 @@ public function transformDimensions(array &$dimensions) { /** * {@inheritdoc} */ + public function transformMimeType(&$mime_type) { + } + + /** + * {@inheritdoc} + */ public function getSummary() { return array( '#theme' => 'image_scale_summary', 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 4cdb149..2866b00 100644 --- a/core/modules/responsive_image/config/schema/responsive_image.schema.yml +++ b/core/modules/responsive_image/config/schema/responsive_image.schema.yml @@ -26,8 +26,24 @@ responsive_image.mappings.*: - type: sequence label: 'Machine name' sequence: - - type: string - label: 'Image style' + - type: mapping + label: 'Mapping definition' + mapping: + mapping_type: + type: string + label: 'Type' + 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/lib/Drupal/responsive_image/Entity/ResponsiveImageMapping.php b/core/modules/responsive_image/lib/Drupal/responsive_image/Entity/ResponsiveImageMapping.php index d94d376..1d4a99c 100644 --- a/core/modules/responsive_image/lib/Drupal/responsive_image/Entity/ResponsiveImageMapping.php +++ b/core/modules/responsive_image/lib/Drupal/responsive_image/Entity/ResponsiveImageMapping.php @@ -144,7 +144,8 @@ protected function loadAllMappings() { $loaded_mappings = $this->getMappings(); $all_mappings = array(); if ($breakpoint_group = $this->getBreakpointGroup()) { - foreach ($breakpoint_group->getBreakpoints() as $breakpoint_id => $breakpoint) { + $breakpoints = $breakpoint_group->getBreakpoints(); + foreach ($breakpoints as $breakpoint_id => $breakpoint) { // Get the components of the breakpoint ID to match the format of the // configuration file. list($source_type, $source, $name) = explode('.', $breakpoint_id); @@ -177,10 +178,11 @@ protected function loadAllMappings() { public function hasMappings() { $mapping_found = FALSE; foreach ($this->getMappings() as $multipliers) { - $filtered_array = array_filter($multipliers); - if (!empty($filtered_array)) { - $mapping_found = TRUE; - break; + foreach ($multipliers as $mapping_definition) { + if (!$this::isEmptyMappingDefinition($mapping_definition)) { + $mapping_found = TRUE; + break 2; + } } } return $mapping_found; @@ -189,6 +191,27 @@ public function hasMappings() { /** * {@inheritdoc} */ + public static function isEmptyMappingDefinition($mapping_definition) { + if (!empty($mapping_definition)) { + switch ($mapping_definition['mapping_type']) { + case 'sizes': + if ($mapping_definition['sizes'] && array_filter($mapping_definition['sizes_image_styles'])) { + return FALSE; + } + break; + case 'image_style': + if ($mapping_definition['image_style']) { + return FALSE; + } + break; + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ public function toArray() { $names = array( 'id', diff --git a/core/modules/responsive_image/lib/Drupal/responsive_image/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php b/core/modules/responsive_image/lib/Drupal/responsive_image/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php index ad4723f..7458d28 100644 --- a/core/modules/responsive_image/lib/Drupal/responsive_image/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php +++ b/core/modules/responsive_image/lib/Drupal/responsive_image/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php @@ -133,6 +133,7 @@ public function viewElements(FieldItemListInterface $items) { $breakpoint_styles = array(); $fallback_image_style = ''; + $image_styles = array(); $responsive_image_mapping = entity_load('responsive_image_mapping', $this->getSetting('responsive_image_mapping')); if ($responsive_image_mapping) { @@ -142,21 +143,24 @@ public function viewElements(FieldItemListInterface $items) { // Make sure that the breakpoint exists and is enabled. // @todo add the following when breakpoint->status is added again: // $responsive_image_mapping->breakpointGroup->breakpoints[$breakpoint_name]->status - $breakpoint = $responsive_image_mapping->getBreakpointGroup()->getBreakpointById($breakpoint_name); + $breakpoint = entity_load('breakpoint', $breakpoint_name); if ($breakpoint) { // Determine the enabled multipliers. $multipliers = array_intersect_key($multipliers, $breakpoint->multipliers); - foreach ($multipliers as $multiplier => $image_style) { + foreach ($multipliers as $multiplier => $mapping_definition) { // Make sure the multiplier still exists. - if (!empty($image_style)) { - // First mapping found is used as fallback. - if (empty($fallback_image_style)) { - $fallback_image_style = $image_style; + if (!$responsive_image_mapping::isEmptyMappingDefinition($mapping_definition)) { + $mapping_definition['sizes_image_styles'] = array_filter($mapping_definition['sizes_image_styles']); + if ($mapping_definition['mapping_type'] == 'image_style') { + $image_styles[] = $mapping_definition['image_style']; + } + else { + $image_styles = array_merge($image_styles, $mapping_definition['sizes_image_styles']); } if (!isset($breakpoint_styles[$breakpoint_name])) { $breakpoint_styles[$breakpoint_name] = array(); } - $breakpoint_styles[$breakpoint_name][$multiplier] = $image_style; + $breakpoint_styles[$breakpoint_name][$multiplier] = $mapping_definition; } } } @@ -168,15 +172,28 @@ public function viewElements(FieldItemListInterface $items) { if ($this->getSetting('fallback_image_style')) { $fallback_image_style = $this->getSetting('fallback_image_style'); } + else { + $fallback_image_style = end($image_styles); + } // Collect cache tags to be added for each item in the field. $all_cache_tags = array(); if ($responsive_image_mapping) { $all_cache_tags[] = $responsive_image_mapping->getCacheTag(); - foreach ($breakpoint_styles as $breakpoint_name => $style_per_multiplier) { - foreach ($style_per_multiplier as $multiplier => $image_style_name) { - $image_style = entity_load('image_style', $image_style_name); - $all_cache_tags[] = $image_style->getCacheTag(); + foreach ($breakpoint_styles as $breakpoint_name => $mapping_definition_per_multiplier) { + foreach ($mapping_definition_per_multiplier as $multiplier => $mapping_definition) { + switch ($mapping_definition['mapping_type']) { + case 'image_style': + $image_style = entity_load('image_style', $mapping_definition['image_style']); + $all_cache_tags[] = $image_style->getCacheTag(); + break; + case 'sizes': + foreach (array_filter($mapping_definition['sizes_image_styles']) as $image_style_name) { + $image_style = entity_load('image_style', $image_style_name); + $all_cache_tags[] = $image_style->getCacheTag(); + } + break; + } } } } diff --git a/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingForm.php b/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingForm.php index 1b4bb4a..10163de 100644 --- a/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingForm.php +++ b/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingForm.php @@ -8,6 +8,7 @@ namespace Drupal\responsive_image; use Drupal\Core\Entity\EntityForm; +use \Drupal\Component\Utility\String; /** * Form controller for the responsive image edit/add forms. @@ -73,16 +74,58 @@ public function form(array $form, array &$form_state) { $image_styles = image_style_options(TRUE); foreach ($responsive_image_mapping->getMappings() as $breakpoint_id => $mapping) { - foreach ($mapping as $multiplier => $image_style) { - $breakpoint = $responsive_image_mapping->getBreakpointGroup()->getBreakpointById($breakpoint_id); - $label = $multiplier . ' ' . $breakpoint->name . ' [' . $breakpoint->mediaQuery . ']'; + foreach ($mapping as $multiplier => $mapping_definition) { + $breakpoint = entity_load('breakpoint', $breakpoint_id); + $label = $multiplier . ' ' . $breakpoint->label . ' [' . $breakpoint->mediaQuery . ']'; $form['mappings'][$breakpoint_id][$multiplier] = array( + '#type' => 'details', + '#title' => String::checkPlain($label), + ); + $form['mappings'][$breakpoint_id][$multiplier]['mapping_type'] = array( + '#title' => t('Type'), + '#type' => 'radios', + '#options' => array( + '' => t('Do not use this breakpoint'), + 'image_style' => t('Use image styles'), + 'sizes' => t('Use the sizes attribute'), + ), + '#default_value' => isset($mapping_definition['mapping_type']) ? $mapping_definition['mapping_type'] : '', + ); + $form['mappings'][$breakpoint_id][$multiplier]['image_style'] = array( '#type' => 'select', - '#title' => check_plain($label), + '#title' => t('Image style'), '#options' => $image_styles, - '#default_value' => $image_style, + '#default_value' => isset($mapping_definition['image_style']) ? $mapping_definition['image_style'] : array(), '#description' => $this->t('Select an image style for this breakpoint.'), + '#states' => array( + 'visible' => array( + ':input[name="mappings[' . $breakpoint_id . '][' . $multiplier . '][mapping_type]"]' => array('value' => 'image_style'), + ), + ), + ); + $form['mappings'][$breakpoint_id][$multiplier]['sizes'] = array( + '#type' => 'textfield', + '#title' => t('Sizes'), + '#default_value' => isset($mapping_definition['sizes']) ? $mapping_definition['sizes'] : '', + '#description' => $this->t('Enter the value for the sizes attribute (e.g. "(min-width:700px) 700px, 100vw").'), + '#states' => array( + 'visible' => array( + ':input[name="mappings[' . $breakpoint_id . '][' . $multiplier . '][mapping_type]"]' => array('value' => 'sizes'), + ), + ), ); + $form['mappings'][$breakpoint_id][$multiplier]['sizes_image_styles'] = array( + '#title' => t('Image styles'), + '#type' => 'checkboxes', + '#options' => array_diff_key($image_styles, array('' => '')), + '#default_value' => isset($mapping_definition['sizes_image_styles']) ? $mapping_definition['sizes_image_styles'] : array(), + '#states' => array( + 'visible' => array( + ':input[name="mappings[' . $breakpoint_id . '][' . $multiplier . '][mapping_type]"]' => array('value' => 'sizes'), + ), + ), + ); + } } diff --git a/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingInterface.php b/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingInterface.php index bb8b535..b24507c 100644 --- a/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingInterface.php +++ b/core/modules/responsive_image/lib/Drupal/responsive_image/ResponsiveImageMappingInterface.php @@ -61,4 +61,14 @@ public function setBreakpointGroup($breakpoint_group); */ public function getBreakpointGroup(); + /** + * Checks if the mapping definition is empty. + * + * @param array $mapping_definition + * The mapping definition to check. + * @return bool + * Whether the mapping definition is considered empty or not. + */ + public static function isEmptyMappingDefinition($mapping_definition); + } diff --git a/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageAdminUITest.php b/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageAdminUITest.php index b752a16..19b62cf 100644 --- a/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageAdminUITest.php +++ b/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageAdminUITest.php @@ -112,30 +112,71 @@ public function testResponsiveImageAdmin() { $this->assertFieldByName('label', 'Mapping One'); $this->assertFieldByName('breakpointGroup', $this->breakpointGroup->id()); - // Check if the dropdows are present for the mappings. - $this->assertFieldByName('mappings[custom.user.small][1x]', ''); - $this->assertFieldByName('mappings[custom.user.small][2x]', ''); - $this->assertFieldByName('mappings[custom.user.medium][1x]', ''); - $this->assertFieldByName('mappings[custom.user.medium][2x]', ''); - $this->assertFieldByName('mappings[custom.user.large][1x]', ''); - $this->assertFieldByName('mappings[custom.user.large][2x]', ''); + // Check if the radio buttons are present. + $this->assertFieldByName('mappings[custom.user.small][1x][mapping_type]', ''); + $this->assertFieldByName('mappings[custom.user.small][2x][mapping_type]', ''); + $this->assertFieldByName('mappings[custom.user.medium][1x][mapping_type]', ''); + $this->assertFieldByName('mappings[custom.user.medium][2x][mapping_type]', ''); + $this->assertFieldByName('mappings[custom.user.large][1x][mapping_type]', ''); + $this->assertFieldByName('mappings[custom.user.large][2x][mapping_type]', ''); + + // Check if the image style dropdowns are present. + $this->assertFieldByName('mappings[custom.user.small][1x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.small][2x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.medium][1x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.medium][2x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.large][1x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.large][2x][image_style]', ''); + + // Check if the sizes textfields are present. + $this->assertFieldByName('mappings[custom.user.small][1x][sizes]', ''); + $this->assertFieldByName('mappings[custom.user.small][2x][sizes]', ''); + $this->assertFieldByName('mappings[custom.user.medium][1x][sizes]', ''); + $this->assertFieldByName('mappings[custom.user.medium][2x][sizes]', ''); + $this->assertFieldByName('mappings[custom.user.large][1x][sizes]', ''); + $this->assertFieldByName('mappings[custom.user.large][2x][sizes]', ''); + + // Check if the image styles checkboxes are present. + foreach (array_keys(image_style_options(FALSE)) as $image_style_name) { + $this->assertFieldByName('mappings[custom.user.small][1x][sizes_image_styles][' . $image_style_name . ']'); + $this->assertFieldByName('mappings[custom.user.small][2x][sizes_image_styles][' . $image_style_name . ']'); + $this->assertFieldByName('mappings[custom.user.medium][1x][sizes_image_styles][' . $image_style_name . ']'); + $this->assertFieldByName('mappings[custom.user.medium][2x][sizes_image_styles][' . $image_style_name . ']'); + $this->assertFieldByName('mappings[custom.user.large][1x][sizes_image_styles][' . $image_style_name . ']'); + $this->assertFieldByName('mappings[custom.user.large][2x][sizes_image_styles][' . $image_style_name . ']'); + } // Save mappings for 1x variant only. $edit = array( 'label' => 'Mapping One', 'breakpointGroup' => $this->breakpointGroup->id(), - 'mappings[custom.user.small][1x]' => 'thumbnail', - 'mappings[custom.user.medium][1x]' => 'medium', - 'mappings[custom.user.large][1x]' => 'large', + 'mappings[custom.user.small][1x][mapping_type]' => 'image_style', + 'mappings[custom.user.small][1x][image_style]' => 'thumbnail', + 'mappings[custom.user.medium][1x][mapping_type]' => 'sizes', + 'mappings[custom.user.medium][1x][sizes]' => '(min-width: 700px) 700px, 100vw', + 'mappings[custom.user.medium][1x][sizes_image_styles][large]' => 'large', + 'mappings[custom.user.medium][1x][sizes_image_styles][medium]' => 'medium', + 'mappings[custom.user.large][1x][mapping_type]' => 'image_style', + 'mappings[custom.user.large][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('mappings[custom.user.small][1x]', 'thumbnail'); - $this->assertFieldByName('mappings[custom.user.small][2x]', ''); - $this->assertFieldByName('mappings[custom.user.medium][1x]', 'medium'); - $this->assertFieldByName('mappings[custom.user.medium][2x]', ''); - $this->assertFieldByName('mappings[custom.user.large][1x]', 'large'); - $this->assertFieldByName('mappings[custom.user.large][2x]', ''); + $this->assertFieldByName('mappings[custom.user.small][1x][image_style]', 'thumbnail'); + $this->assertFieldByName('mappings[custom.user.small][1x][mapping_type]', 'image_style'); + $this->assertFieldByName('mappings[custom.user.small][2x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.small][2x][mapping_type]', ''); + $this->assertFieldByName('mappings[custom.user.medium][1x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.medium][1x][mapping_type]', 'sizes'); + $this->assertFieldByName('mappings[custom.user.medium][1x][sizes]', '(min-width: 700px) 700px, 100vw'); + $this->assertFieldChecked('edit-mappings-customusermedium-1x-sizes-image-styles-large'); + $this->assertFieldChecked('edit-mappings-customusermedium-1x-sizes-image-styles-medium'); + $this->assertNoFieldChecked('edit-mappings-customusermedium-1x-sizes-image-styles-thumbnail'); + $this->assertFieldByName('mappings[custom.user.medium][2x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.medium][2x][mapping_type]', ''); + $this->assertFieldByName('mappings[custom.user.large][1x][image_style]', 'large'); + $this->assertFieldByName('mappings[custom.user.large][1x][mapping_type]', 'image_style'); + $this->assertFieldByName('mappings[custom.user.large][2x][image_style]', ''); + $this->assertFieldByName('mappings[custom.user.large][2x][mapping_type]', ''); // Delete the mapping. $this->drupalGet('admin/config/media/responsive-image-mapping/mapping_one/delete'); diff --git a/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageFieldDisplayTest.php index 5d34845..f2a44dd 100644 --- a/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/lib/Drupal/responsive_image/Tests/ResponsiveImageFieldDisplayTest.php @@ -91,9 +91,27 @@ public function setUp() { )); $responsive_image_mapping->save(); $mappings = array(); - $mappings['custom.user.small']['1x'] = 'thumbnail'; - $mappings['custom.user.medium']['1x'] = 'medium'; - $mappings['custom.user.large']['1x'] = 'large'; + $mappings['custom.user.small']['1x'] = array( + 'mapping_type' => 'image_style', + 'image_style' => 'thumbnail', + 'sizes' => '', + 'sizes_image_styles' => array(), + ); + $mappings['custom.user.medium']['1x'] = array( + 'mapping_type' => 'sizes', + 'image_style' => '', + 'sizes' => '(min-width: 700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + 'medium' => 'medium', + ), + ); + $mappings['custom.user.large']['1x'] = array( + 'mapping_type' => 'image_style', + 'image_style' => 'large', + 'sizes' => '', + 'sizes_image_styles' => array(), + ); $responsive_image_mapping->setMappings($mappings); $responsive_image_mapping->save(); } @@ -183,6 +201,10 @@ 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 = entity_load('image_style', '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/'); @@ -191,6 +213,9 @@ public function _testResponsiveImageFieldFormatters($scheme) { $this->assertRaw('media="(min-width: 200px)"'); $this->assertRaw('media="(min-width: 400px)"'); $this->assertRaw('media="(min-width: 600px)"'); + $this->assertRaw('sizes="(min-width: 700px) 700px, 100vw"'); + // Check for the MIME-type. + $this->assertRaw('type="image/png"'); $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)); @@ -198,14 +223,20 @@ public function _testResponsiveImageFieldFormatters($scheme) { $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); + $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' => 480, 'height' => 240), + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => $large_style->buildUrl($image->getSource()), + 'width' => $dimensions['width'], + ), + ), ); $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 thumbnail formatter displaying correctly on full node view.'*/); if ($scheme == 'private') { // Log out and try to access the file. diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 82ecafe..3f6c996 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -115,10 +115,10 @@ function responsive_image_theme() { ), 'responsive_image_source' => array( 'variables' => array( - 'src' => NULL, 'srcset' => NULL, - 'dimensions' => NULL, 'media' => NULL, + 'mime_type' => NULL, + 'sizes' => NULL, ), ), ); @@ -209,67 +209,85 @@ function theme_responsive_image($variables) { $sources = array(); $output = array(); - - // Fallback image, output as source with media query. - $sources[] = array( - 'src' => entity_load('image_style', $variables['style_name'])->buildUrl($variables['uri']), - 'dimensions' => responsive_image_get_image_dimensions($variables), - ); + $image = \Drupal::service('image.factory')->get($variables['uri']); + $mime_type = $image->getMimeType(); // All breakpoints and multipliers. foreach ($variables['breakpoints'] as $breakpoint_name => $multipliers) { $breakpoint = breakpoint_load($breakpoint_name); if ($breakpoint) { - $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['mapping_type']) { + case 'sizes': + foreach (array_filter($mapping_definition['sizes_image_styles']) as $image_style_name) { + $image_style = entity_load('image_style', $image_style_name); - // Only one image, use src. - if (count($new_sources) == 1) { - $sources[] = array( - 'src' => entity_load('image_style', $new_sources[0]['style_name'])->buildUrl($new_sources[0]['uri']), - 'dimensions' => responsive_image_get_image_dimensions($new_sources[0]), - 'media' => $breakpoint->mediaQuery, - ); - } - else { - // Multiple images, use srcset. - $srcset = array(); - foreach ($new_sources as $new_source) { - $srcset[] = entity_load('image_style', $new_source['style_name'])->buildUrl($new_source['uri']) . ' ' . $new_source['#multiplier']; + // Get dimensions. + $dimensions = array('width' => $image->getWidth(), 'height' => $image->getHeight()); + $image_style->transformDimensions($dimensions); + + // Get mime type. + $derivative_mime_type = $mime_type; + $image_style->transformMimeType($derivative_mime_type); + $derivative_mime_types[] = $derivative_mime_type; + + $srcset[] = $image_style->buildUrl($image->getSource()) . ' ' . $dimensions['width'] . 'w'; + $sizes = array_merge(explode(',', $mapping_definition['sizes']), $sizes); + } + break; + case 'image_style': + $image_style = entity_load('image_style', $mapping_definition['image_style']); + + // Get mime type. + $derivative_mime_type = $mime_type; + $image_style->transformMimeType($derivative_mime_type); + $derivative_mime_types[] = $derivative_mime_type; + + $srcset[] = $image_style->buildUrl($image->getSource()) . ' ' . $multiplier; + break; } - $sources[] = array( - 'srcset' => implode(', ', $srcset), - 'dimensions' => responsive_image_get_image_dimensions($new_sources[0]), - 'media' => $breakpoint->mediaQuery, - ); } + // Only add mime type if it is unique. + $derivative_mime_types = array_unique($derivative_mime_types); + + $sources[] = array( + '#theme' => 'responsive_image_source', + '#srcset' => implode(', ', array_unique($srcset)), + '#media' => $breakpoint->mediaQuery, + '#mime_type' => count($derivative_mime_types) == 1 ? reset($derivative_mime_types) : '', + '#sizes' => implode(',', array_unique($sizes)), + ); } } if (!empty($sources)) { $output[] = ''; + $output = array_merge($output, array_map('drupal_render', $sources)); + // Output the fallback image. + $fallback_image_style = entity_load('image_style', $variables['style_name']); + $dimensions = array('width' => $image->getWidth(), 'height' => $image->getHeight()); + $fallback_image_style->transformDimensions($dimensions); - // 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' => $fallback_image_style->buildUrl($image->getSource()), + 'width' => $dimensions['width'], + ), + ), + ); + 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); $output[] = ''; return implode("\n", $output); } @@ -285,50 +303,23 @@ function theme_responsive_image($variables) { * 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 */ function theme_responsive_image_source($variables) { - $output = array(); - if (isset($variables['media']) && !empty($variables['media'])) { - if (!isset($variables['srcset'])) { - $output[] = ''; - $output[] = ''; - } - elseif (!isset($variables['src'])) { - $output[] = ''; - $output[] = ''; - } - } - else { - $output[] = ''; - $output[] = ''; - } - return implode("\n", $output); -} - -/** - * Determines the dimensions of an image. - * - * @param $variables - * 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). - * - * @return array - * Dimensions to be modified - an array with components width and height, in - * pixels. - */ -function responsive_image_get_image_dimensions($variables) { - // Determine the dimensions of the styled image. - $dimensions = array( - 'width' => $variables['width'], - 'height' => $variables['height'], + $attributes = array( + 'srcset' => $variables['srcset'], ); - entity_load('image_style', $variables['style_name'])->transformDimensions($dimensions); + if (isset($variables['media']) && !empty($variables['media'])) { + $attributes['media'] = $variables['media']; + } - return $dimensions; + if (isset($variables['mime_type']) && !empty($variables['mime_type'])) { + $attributes['type'] = $variables['mime_type']; + } + if (isset($variables['sizes']) && !empty($variables['sizes'])) { + $attributes['sizes'] = $variables['sizes']; + } + return ''; } diff --git a/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.narrow.yml b/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.narrow.yml index 5fae446..0c307cc 100644 --- a/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.narrow.yml +++ b/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.narrow.yml @@ -4,7 +4,7 @@ label: narrow mediaQuery: 'only screen and (min-width: 16.5em)' source: toolbar sourceType: module -weight: 0 +weight: 2 multipliers: 1x: 1x status: true diff --git a/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.wide.yml b/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.wide.yml index bf6243e..fcfde54 100644 --- a/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.wide.yml +++ b/core/modules/toolbar/config/install/breakpoint.breakpoint.module.toolbar.wide.yml @@ -4,7 +4,7 @@ label: wide mediaQuery: 'only screen and (min-width: 52em)' source: toolbar sourceType: module -weight: 2 +weight: 0 multipliers: 1x: 1x status: true diff --git a/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.mobile.yml b/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.mobile.yml index 7add4c0..6d5b659 100644 --- a/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.mobile.yml +++ b/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.mobile.yml @@ -4,7 +4,7 @@ label: mobile mediaQuery: '(min-width: 0px)' source: bartik sourceType: theme -weight: 0 +weight: 2 multipliers: 1x: 1x status: true diff --git a/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.wide.yml b/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.wide.yml index d43f1ae..e3ec51e 100644 --- a/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.wide.yml +++ b/core/themes/bartik/config/install/breakpoint.breakpoint.theme.bartik.wide.yml @@ -4,7 +4,7 @@ label: wide mediaQuery: 'all and (min-width: 851px)' source: bartik sourceType: theme -weight: 2 +weight: 0 multipliers: 1x: 1x status: true diff --git a/core/themes/bartik/config/install/breakpoint.breakpoint_group.theme.bartik.bartik.yml b/core/themes/bartik/config/install/breakpoint.breakpoint_group.theme.bartik.bartik.yml index a7d85fc..09212eb 100644 --- a/core/themes/bartik/config/install/breakpoint.breakpoint_group.theme.bartik.bartik.yml +++ b/core/themes/bartik/config/install/breakpoint.breakpoint_group.theme.bartik.bartik.yml @@ -5,6 +5,7 @@ breakpoint_ids: - theme.bartik.mobile - theme.bartik.narrow - theme.bartik.wide + - module.breakpoint._none source: bartik sourceType: theme status: true