diff --git a/core/assets/vendor/picturefill/picturefill.js b/core/assets/vendor/picturefill/picturefill.js index 4d69569..db71d5f 100644 --- a/core/assets/vendor/picturefill/picturefill.js +++ b/core/assets/vendor/picturefill/picturefill.js @@ -1,126 +1,659 @@ -/*jshint loopfunc: true, browser: true, curly: true, eqeqeq: true, expr: true, forin: true, latedef: true, newcap: true, noarg: true, trailing: true, undef: true, unused: true */ -/*! Picturefill - Author: Scott Jehl, 2012 | License: MIT/GPLv2 */ -(function (w) { - - // Enable strict mode. - "use strict"; - - // Test if `` is supported natively, if so, exit. - if (!!(w.document.createElement('picture') && w.document.createElement('source') && w.HTMLPictureElement)) { - return; - } - - w.picturefill = function () { - // Copy attributes from the source to the destination. - function _copyAttributes(src, tar) { - if (src.getAttribute('width') && src.getAttribute('height')) { - tar.width = src.getAttribute('width'); - tar.height = src.getAttribute('height'); - } - } - - // Get all picture tags. - var ps = w.document.getElementsByTagName('picture'); - - // Loop the pictures. - for (var i = 0, il = ps.length; i < il; i++) { - var sources = ps[i].getElementsByTagName('source'); - var picImg = null; - var matches = []; - - // If no sources are found, they're likely erased from the DOM. - // Try finding them inside comments. - if (!sources.length) { - var picText = ps[i].innerHTML; - var frag = w.document.createElement('div'); - // For IE9, convert the source elements to divs. - var srcs = picText.replace(/(<)source([^>]+>)/gmi, '$1div$2').match(/]+>/gmi); - - frag.innerHTML = srcs.join(''); - sources = frag.getElementsByTagName('div'); - } - - // See which sources match. - for (var j = 0, jl = sources.length; j < jl; j++) { - var media = sources[j].getAttribute('media'); - // If there's no media specified or the media query matches, add it. - if (!media || (w.matchMedia && w.matchMedia(media).matches)) { - matches.push(sources[j]); - } - } - - if (matches.length) { - // Grab the most appropriate (last) match. - var match = matches.pop(); - var srcset = match.getAttribute('srcset'); - - // Find any existing img element in the picture element. - picImg = ps[i].getElementsByTagName('img')[0]; - - // Add a new img element if one doesn't exists. - if (!picImg) { - picImg = w.document.createElement('img'); - picImg.alt = ps[i].getAttribute('alt'); - ps[i].appendChild(picImg); - } - - // Source element uses a srcset. - if (srcset) { - var screenRes = w.devicePixelRatio || 1; - // Split comma-separated `srcset` sources into an array. - sources = srcset.split(', '); - - // Loop through each source/resolution in srcset. - for (var res = sources.length, r = res - 1; r >= 0; r--) { - // Remove any leading whitespace, then split on spaces. - var source = sources[ r ].replace(/^\s*/, '').replace(/\s*$/, '').split(' '); - // Parse out the resolution for each source in `srcset`. - var resMatch = parseFloat(source[1], 10); - - if (screenRes >= resMatch) { - if (picImg.getAttribute('src') !== source[0]) { - var newImg = document.createElement('img'); - - newImg.src = source[0]; - // When the image is loaded, set a width equal to that of the - // original’s intrinsic width divided by the screen resolution. - newImg.onload = function () { - // Clone the original image into memory so the width is - // unaffected by page styles. - var w = this.cloneNode(true).width; - if (w > 0) { - this.width = (w / resMatch); - } - }; - // Copy width and height from the source tag to the img element. - _copyAttributes(match, newImg); - picImg.parentNode.replaceChild(newImg, picImg); - } - // We’ve matched, so bail out of the loop here. - break; - } - } - } else { - // No srcset used, so just use the 'src' value. - picImg.src = match.getAttribute('src'); - // Copy width and height from the source tag to the img element. - _copyAttributes(match, picImg); - } - } - } - }; - - // Run on resize and domready (w.load as a fallback) - if (w.addEventListener) { - w.addEventListener('resize', w.picturefill, false); - w.addEventListener('DOMContentLoaded', function () { - w.picturefill(); - // Run once only. - w.removeEventListener('load', w.picturefill, false); - }, false); - w.addEventListener('load', w.picturefill, false); - } - else if (w.attachEvent) { - w.attachEvent('onload', w.picturefill); - } -})(this); +/*! Picturefill - v2.2.1 - 2015-02-03 +* http://scottjehl.github.io/picturefill +* Copyright (c) 2015 https://github.com/scottjehl/picturefill/blob/master/Authors.txt; Licensed MIT */ +/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. Dual MIT/BSD license */ + +window.matchMedia || (window.matchMedia = function() { + "use strict"; + + // For browsers that support matchMedium api such as IE 9 and webkit + var styleMedia = (window.styleMedia || window.media); + + // For those that don't support matchMedium + if (!styleMedia) { + var style = document.createElement('style'), + script = document.getElementsByTagName('script')[0], + info = null; + + style.type = 'text/css'; + style.id = 'matchmediajs-test'; + + script.parentNode.insertBefore(style, script); + + // 'style.currentStyle' is used by IE <= 8 and 'window.getComputedStyle' for all other browsers + info = ('getComputedStyle' in window) && window.getComputedStyle(style, null) || style.currentStyle; + + styleMedia = { + matchMedium: function(media) { + var text = '@media ' + media + '{ #matchmediajs-test { width: 1px; } }'; + + // 'style.styleSheet' is used by IE <= 8 and 'style.textContent' for all other browsers + if (style.styleSheet) { + style.styleSheet.cssText = text; + } else { + style.textContent = text; + } + + // Test if media query is true or false + return info.width === '1px'; + } + }; + } + + return function(media) { + return { + matches: styleMedia.matchMedium(media || 'all'), + media: media || 'all' + }; + }; +}()); +/*! Picturefill - Responsive Images that work today. +* Author: Scott Jehl, Filament Group, 2012 ( new proposal implemented by Shawn Jansepar ) +* License: MIT/GPLv2 +* Spec: http://picture.responsiveimages.org/ +*/ +(function( w, doc, image ) { + // Enable strict mode + "use strict"; + + // If picture is supported, well, that's awesome. Let's get outta here... + if ( w.HTMLPictureElement ) { + w.picturefill = function() { }; + return; + } + + // HTML shim|v it for old IE (IE9 will still need the HTML video tag workaround) + doc.createElement( "picture" ); + + // local object for method references and testing exposure + var pf = {}; + + // namespace + pf.ns = "picturefill"; + + // srcset support test + (function() { + pf.srcsetSupported = "srcset" in image; + pf.sizesSupported = "sizes" in image; + })(); + + // just a string trim workaround + pf.trim = function( str ) { + return str.trim ? str.trim() : str.replace( /^\s+|\s+$/g, "" ); + }; + + // just a string endsWith workaround + pf.endsWith = function( str, suffix ) { + return str.endsWith ? str.endsWith( suffix ) : str.indexOf( suffix, str.length - suffix.length ) !== -1; + }; + + /** + * Shortcut method for https://w3c.github.io/webappsec/specs/mixedcontent/#restricts-mixed-content ( for easy overriding in tests ) + */ + pf.restrictsMixedContent = function() { + return w.location.protocol === "https:"; + }; + /** + * Shortcut method for matchMedia ( for easy overriding in tests ) + */ + + pf.matchesMedia = function( media ) { + return w.matchMedia && w.matchMedia( media ).matches; + }; + + // Shortcut method for `devicePixelRatio` ( for easy overriding in tests ) + pf.getDpr = function() { + return ( w.devicePixelRatio || 1 ); + }; + + /** + * Get width in css pixel value from a "length" value + * http://dev.w3.org/csswg/css-values-3/#length-value + */ + pf.getWidthFromLength = function( length ) { + // If a length is specified and doesn’t contain a percentage, and it is greater than 0 or using `calc`, use it. Else, use the `100vw` default. + length = length && length.indexOf( "%" ) > -1 === false && ( parseFloat( length ) > 0 || length.indexOf( "calc(" ) > -1 ) ? length : "100vw"; + + /** + * If length is specified in `vw` units, use `%` instead since the div we’re measuring + * is injected at the top of the document. + * + * TODO: maybe we should put this behind a feature test for `vw`? + */ + length = length.replace( "vw", "%" ); + + // Create a cached element for getting length value widths + if ( !pf.lengthEl ) { + pf.lengthEl = doc.createElement( "div" ); + + // Positioning styles help prevent padding/margin/width on `html` or `body` from throwing calculations off. + pf.lengthEl.style.cssText = "border:0;display:block;font-size:1em;left:0;margin:0;padding:0;position:absolute;visibility:hidden"; + } + + pf.lengthEl.style.width = length; + + doc.body.appendChild(pf.lengthEl); + + // Add a class, so that everyone knows where this element comes from + pf.lengthEl.className = "helper-from-picturefill-js"; + + if ( pf.lengthEl.offsetWidth <= 0 ) { + // Something has gone wrong. `calc()` is in use and unsupported, most likely. Default to `100vw` (`100%`, for broader support.): + pf.lengthEl.style.width = doc.documentElement.offsetWidth + "px"; + } + + var offsetWidth = pf.lengthEl.offsetWidth; + + doc.body.removeChild( pf.lengthEl ); + + return offsetWidth; + }; + + // container of supported mime types that one might need to qualify before using + pf.types = {}; + + // Add support for standard mime types + pf.types[ "image/jpeg" ] = true; + pf.types[ "image/gif" ] = true; + pf.types[ "image/png" ] = true; + + // test svg support + pf.types[ "image/svg+xml" ] = doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1"); + + // test webp support, only when the markup calls for it + pf.types[ "image/webp" ] = function() { + // based on Modernizr's lossless img-webp test + // note: asynchronous + var type = "image/webp"; + + image.onerror = function() { + pf.types[ type ] = false; + picturefill(); + }; + image.onload = function() { + pf.types[ type ] = image.width === 1; + picturefill(); + }; + image.src = "data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="; + }; + + /** + * Takes a source element and checks if its type attribute is present and if so, supported + * Note: for type tests that require a async logic, + * you can define them as a function that'll run only if that type needs to be tested. Just make the test function call picturefill again when it is complete. + * see the async webp test above for example + */ + pf.verifyTypeSupport = function( source ) { + var type = source.getAttribute( "type" ); + // if type attribute exists, return test result, otherwise return true + if ( type === null || type === "" ) { + return true; + } else { + // if the type test is a function, run it and return "pending" status. The function will rerun picturefill on pending elements once finished. + if ( typeof( pf.types[ type ] ) === "function" ) { + pf.types[ type ](); + return "pending"; + } else { + return pf.types[ type ]; + } + } + }; + + // Parses an individual `size` and returns the length, and optional media query + pf.parseSize = function( sourceSizeStr ) { + var match = /(\([^)]+\))?\s*(.+)/g.exec( sourceSizeStr ); + return { + media: match && match[1], + length: match && match[2] + }; + }; + + // Takes a string of sizes and returns the width in pixels as a number + pf.findWidthFromSourceSize = function( sourceSizeListStr ) { + // Split up source size list, ie ( max-width: 30em ) 100%, ( max-width: 50em ) 50%, 33% + // or (min-width:30em) calc(30% - 15px) + var sourceSizeList = pf.trim( sourceSizeListStr ).split( /\s*,\s*/ ), + winningLength; + + for ( var i = 0, len = sourceSizeList.length; i < len; i++ ) { + // Match ? length, ie ( min-width: 50em ) 100% + var sourceSize = sourceSizeList[ i ], + // Split "( min-width: 50em ) 100%" into separate strings + parsedSize = pf.parseSize( sourceSize ), + length = parsedSize.length, + media = parsedSize.media; + + if ( !length ) { + continue; + } + if ( !media || pf.matchesMedia( media ) ) { + // if there is no media query or it matches, choose this as our winning length + // and end algorithm + winningLength = length; + break; + } + } + + // pass the length to a method that can properly determine length + // in pixels based on these formats: http://dev.w3.org/csswg/css-values-3/#length-value + return pf.getWidthFromLength( winningLength ); + }; + + pf.parseSrcset = function( srcset ) { + /** + * A lot of this was pulled from Boris Smus’ parser for the now-defunct WHATWG `srcset` + * https://github.com/borismus/srcset-polyfill/blob/master/js/srcset-info.js + * + * 1. Let input (`srcset`) be the value passed to this algorithm. + * 2. Let position be a pointer into input, initially pointing at the start of the string. + * 3. Let raw candidates be an initially empty ordered list of URLs with associated + * unparsed descriptors. The order of entries in the list is the order in which entries + * are added to the list. + */ + var candidates = []; + + while ( srcset !== "" ) { + srcset = srcset.replace( /^\s+/g, "" ); + + // 5. Collect a sequence of characters that are not space characters, and let that be url. + var pos = srcset.search(/\s/g), + url, descriptor = null; + + if ( pos !== -1 ) { + url = srcset.slice( 0, pos ); + + var last = url.slice(-1); + + // 6. If url ends with a U+002C COMMA character (,), remove that character from url + // and let descriptors be the empty string. Otherwise, follow these substeps + // 6.1. If url is empty, then jump to the step labeled descriptor parser. + + if ( last === "," || url === "" ) { + url = url.replace( /,+$/, "" ); + descriptor = ""; + } + srcset = srcset.slice( pos + 1 ); + + // 6.2. Collect a sequence of characters that are not U+002C COMMA characters (,), and + // let that be descriptors. + if ( descriptor === null ) { + var descpos = srcset.indexOf( "," ); + if ( descpos !== -1 ) { + descriptor = srcset.slice( 0, descpos ); + srcset = srcset.slice( descpos + 1 ); + } else { + descriptor = srcset; + srcset = ""; + } + } + } else { + url = srcset; + srcset = ""; + } + + // 7. Add url to raw candidates, associated with descriptors. + if ( url || descriptor ) { + candidates.push({ + url: url, + descriptor: descriptor + }); + } + } + return candidates; + }; + + pf.parseDescriptor = function( descriptor, sizesattr ) { + // 11. Descriptor parser: Let candidates be an initially empty source set. The order of entries in the list + // is the order in which entries are added to the list. + var sizes = sizesattr || "100vw", + sizeDescriptor = descriptor && descriptor.replace( /(^\s+|\s+$)/g, "" ), + widthInCssPixels = pf.findWidthFromSourceSize( sizes ), + resCandidate; + + if ( sizeDescriptor ) { + var splitDescriptor = sizeDescriptor.split(" "); + + for (var i = splitDescriptor.length - 1; i >= 0; i--) { + var curr = splitDescriptor[ i ], + lastchar = curr && curr.slice( curr.length - 1 ); + + if ( ( lastchar === "h" || lastchar === "w" ) && !pf.sizesSupported ) { + resCandidate = parseFloat( ( parseInt( curr, 10 ) / widthInCssPixels ) ); + } else if ( lastchar === "x" ) { + var res = curr && parseFloat( curr, 10 ); + resCandidate = res && !isNaN( res ) ? res : 1; + } + } + } + return resCandidate || 1; + }; + + /** + * Takes a srcset in the form of url/ + * ex. "images/pic-medium.png 1x, images/pic-medium-2x.png 2x" or + * "images/pic-medium.png 400w, images/pic-medium-2x.png 800w" or + * "images/pic-small.png" + * Get an array of image candidates in the form of + * {url: "/foo/bar.png", resolution: 1} + * where resolution is http://dev.w3.org/csswg/css-values-3/#resolution-value + * If sizes is specified, resolution is calculated + */ + pf.getCandidatesFromSourceSet = function( srcset, sizes ) { + var candidates = pf.parseSrcset( srcset ), + formattedCandidates = []; + + for ( var i = 0, len = candidates.length; i < len; i++ ) { + var candidate = candidates[ i ]; + + formattedCandidates.push({ + url: candidate.url, + resolution: pf.parseDescriptor( candidate.descriptor, sizes ) + }); + } + return formattedCandidates; + }; + + /** + * if it's an img element and it has a srcset property, + * we need to remove the attribute so we can manipulate src + * (the property's existence infers native srcset support, and a srcset-supporting browser will prioritize srcset's value over our winning picture candidate) + * this moves srcset's value to memory for later use and removes the attr + */ + pf.dodgeSrcset = function( img ) { + if ( img.srcset ) { + img[ pf.ns ].srcset = img.srcset; + img.removeAttribute( "srcset" ); + } + }; + + // Accept a source or img element and process its srcset and sizes attrs + pf.processSourceSet = function( el ) { + var srcset = el.getAttribute( "srcset" ), + sizes = el.getAttribute( "sizes" ), + candidates = []; + + // if it's an img element, use the cached srcset property (defined or not) + if ( el.nodeName.toUpperCase() === "IMG" && el[ pf.ns ] && el[ pf.ns ].srcset ) { + srcset = el[ pf.ns ].srcset; + } + + if ( srcset ) { + candidates = pf.getCandidatesFromSourceSet( srcset, sizes ); + } + return candidates; + }; + + pf.applyBestCandidate = function( candidates, picImg ) { + var candidate, + length, + bestCandidate; + + candidates.sort( pf.ascendingSort ); + + length = candidates.length; + bestCandidate = candidates[ length - 1 ]; + + for ( var i = 0; i < length; i++ ) { + candidate = candidates[ i ]; + if ( candidate.resolution >= pf.getDpr() ) { + bestCandidate = candidate; + break; + } + } + + if ( bestCandidate && !pf.endsWith( picImg.src, bestCandidate.url ) ) { + if ( pf.restrictsMixedContent() && bestCandidate.url.substr(0, "http:".length).toLowerCase() === "http:" ) { + if ( typeof console !== undefined ) { + console.warn( "Blocked mixed content image " + bestCandidate.url ); + } + } else { + picImg.src = bestCandidate.url; + // currentSrc attribute and property to match + // http://picture.responsiveimages.org/#the-img-element + picImg.currentSrc = picImg.src; + + var style = picImg.style || {}, + WebkitBackfaceVisibility = "webkitBackfaceVisibility" in style, + currentZoom = style.zoom; + + if (WebkitBackfaceVisibility) { // See: https://github.com/scottjehl/picturefill/issues/332 + style.zoom = ".999"; + + WebkitBackfaceVisibility = picImg.offsetWidth; + + style.zoom = currentZoom; + } + } + } + }; + + pf.ascendingSort = function( a, b ) { + return a.resolution - b.resolution; + }; + + /** + * In IE9, elements get removed if they aren't children of + * video elements. Thus, we conditionally wrap source elements + * using + * and must account for that here by moving those source elements + * back into the picture element. + */ + pf.removeVideoShim = function( picture ) { + var videos = picture.getElementsByTagName( "video" ); + if ( videos.length ) { + var video = videos[ 0 ], + vsources = video.getElementsByTagName( "source" ); + while ( vsources.length ) { + picture.insertBefore( vsources[ 0 ], video ); + } + // Remove the video element once we're finished removing its children + video.parentNode.removeChild( video ); + } + }; + + /** + * Find all `img` elements, and add them to the candidate list if they have + * a `picture` parent, a `sizes` attribute in basic `srcset` supporting browsers, + * a `srcset` attribute at all, and they haven’t been evaluated already. + */ + pf.getAllElements = function() { + var elems = [], + imgs = doc.getElementsByTagName( "img" ); + + for ( var h = 0, len = imgs.length; h < len; h++ ) { + var currImg = imgs[ h ]; + + if ( currImg.parentNode.nodeName.toUpperCase() === "PICTURE" || + ( currImg.getAttribute( "srcset" ) !== null ) || currImg[ pf.ns ] && currImg[ pf.ns ].srcset !== null ) { + elems.push( currImg ); + } + } + return elems; + }; + + pf.getMatch = function( img, picture ) { + var sources = picture.childNodes, + match; + + // Go through each child, and if they have media queries, evaluate them + for ( var j = 0, slen = sources.length; j < slen; j++ ) { + var source = sources[ j ]; + + // ignore non-element nodes + if ( source.nodeType !== 1 ) { + continue; + } + + // Hitting the `img` element that started everything stops the search for `sources`. + // If no previous `source` matches, the `img` itself is evaluated later. + if ( source === img ) { + return match; + } + + // ignore non-`source` nodes + if ( source.nodeName.toUpperCase() !== "SOURCE" ) { + continue; + } + // if it's a source element that has the `src` property set, throw a warning in the console + if ( source.getAttribute( "src" ) !== null && typeof console !== undefined ) { + console.warn("The `src` attribute is invalid on `picture` `source` element; instead, use `srcset`."); + } + + var media = source.getAttribute( "media" ); + + // if source does not have a srcset attribute, skip + if ( !source.getAttribute( "srcset" ) ) { + continue; + } + + // if there's no media specified, OR w.matchMedia is supported + if ( ( !media || pf.matchesMedia( media ) ) ) { + var typeSupported = pf.verifyTypeSupport( source ); + + if ( typeSupported === true ) { + match = source; + break; + } else if ( typeSupported === "pending" ) { + return false; + } + } + } + + return match; + }; + + function picturefill( opt ) { + var elements, + element, + parent, + firstMatch, + candidates, + options = opt || {}; + + elements = options.elements || pf.getAllElements(); + + // Loop through all elements + for ( var i = 0, plen = elements.length; i < plen; i++ ) { + element = elements[ i ]; + parent = element.parentNode; + firstMatch = undefined; + candidates = undefined; + + // immediately skip non-`img` nodes + if ( element.nodeName.toUpperCase() !== "IMG" ) { + continue; + } + + // expando for caching data on the img + if ( !element[ pf.ns ] ) { + element[ pf.ns ] = {}; + } + + // if the element has already been evaluated, skip it unless + // `options.reevaluate` is set to true ( this, for example, + // is set to true when running `picturefill` on `resize` ). + if ( !options.reevaluate && element[ pf.ns ].evaluated ) { + continue; + } + + // if `img` is in a `picture` element + if ( parent.nodeName.toUpperCase() === "PICTURE" ) { + + // IE9 video workaround + pf.removeVideoShim( parent ); + + // return the first match which might undefined + // returns false if there is a pending source + // TODO the return type here is brutal, cleanup + firstMatch = pf.getMatch( element, parent ); + + // if any sources are pending in this picture due to async type test(s) + // remove the evaluated attr and skip for now ( the pending test will + // rerun picturefill on this element when complete) + if ( firstMatch === false ) { + continue; + } + } else { + firstMatch = undefined; + } + + // Cache and remove `srcset` if present and we’re going to be doing `picture`/`srcset`/`sizes` polyfilling to it. + if ( parent.nodeName.toUpperCase() === "PICTURE" || + ( element.srcset && !pf.srcsetSupported ) || + ( !pf.sizesSupported && ( element.srcset && element.srcset.indexOf("w") > -1 ) ) ) { + pf.dodgeSrcset( element ); + } + + if ( firstMatch ) { + candidates = pf.processSourceSet( firstMatch ); + pf.applyBestCandidate( candidates, element ); + } else { + // No sources matched, so we’re down to processing the inner `img` as a source. + candidates = pf.processSourceSet( element ); + + if ( element.srcset === undefined || element[ pf.ns ].srcset ) { + // Either `srcset` is completely unsupported, or we need to polyfill `sizes` functionality. + pf.applyBestCandidate( candidates, element ); + } // Else, resolution-only `srcset` is supported natively. + } + + // set evaluated to true to avoid unnecessary reparsing + element[ pf.ns ].evaluated = true; + } + } + + /** + * Sets up picture polyfill by polling the document and running + * the polyfill every 250ms until the document is ready. + * Also attaches picturefill on resize + */ + function runPicturefill() { + picturefill(); + var intervalId = setInterval( function() { + // When the document has finished loading, stop checking for new images + // https://github.com/ded/domready/blob/master/ready.js#L15 + picturefill(); + if ( /^loaded|^i|^c/.test( doc.readyState ) ) { + clearInterval( intervalId ); + return; + } + }, 250 ); + + function checkResize() { + var resizeThrottle; + + if ( !w._picturefillWorking ) { + w._picturefillWorking = true; + w.clearTimeout( resizeThrottle ); + resizeThrottle = w.setTimeout( function() { + picturefill({ reevaluate: true }); + w._picturefillWorking = false; + }, 60 ); + } + } + + if ( w.addEventListener ) { + w.addEventListener( "resize", checkResize, false ); + } else if ( w.attachEvent ) { + w.attachEvent( "onresize", checkResize ); + } + } + + runPicturefill(); + + /* expose methods for testing */ + picturefill._ = pf; + + /* expose picturefill */ + if ( typeof module === "object" && typeof module.exports === "object" ) { + // CommonJS, just export + module.exports = picturefill; + } else if ( typeof define === "function" && define.amd ) { + // AMD support + define( function() { return picturefill; } ); + } else if ( typeof w === "object" ) { + // If no AMD and we are in the browser, attach to window + w.picturefill = picturefill; + } + +} )( window, window.document, new window.Image() ); diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 825ed77..769ac28 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -834,12 +834,10 @@ normalize: picturefill: remote: https://github.com/scottjehl/picturefill - # @todo Contribute upstream and/or replace with upstream version. - # @see https://drupal.org/node/1775530 - version: VERSION + version: 2.2.1 license: name: MIT - url: https://github.com/scottjehl/picturefill/blob/master/LICENSE + url: https://github.com/scottjehl/picturefill/blob/2.2.0/LICENSE gpl-compatible: true js: assets/vendor/picturefill/picturefill.js: { weight: -10 } diff --git a/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php b/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php index a8a46f9..83986eb 100644 --- a/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php +++ b/core/modules/breakpoint/src/Tests/BreakpointDiscoveryTest.php @@ -33,54 +33,54 @@ protected function setUp() { public function testThemeBreakpoints() { // Verify the breakpoint group for breakpoint_theme_test was created. $expected_breakpoints = array( - 'breakpoint_theme_test.mobile' => array( - 'label' => 'mobile', - 'mediaQuery' => '(min-width: 0px)', - 'weight' => 0, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.mobile', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'breakpoint_theme_test.tv' => array( + 'label' => 'tv', + 'mediaQuery' => 'only screen and (min-width: 1220px)', + 'weight' => 0, + 'multipliers' => array( + '1x', ), - 'breakpoint_theme_test.narrow' => array( - 'label' => 'narrow', - 'mediaQuery' => '(min-width: 560px)', - 'weight' => 1, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.narrow', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.tv', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), + 'breakpoint_theme_test.wide' => array( + 'label' => 'wide', + 'mediaQuery' => '(min-width: 851px)', + 'weight' => 1, + 'multipliers' => array( + '1x', ), - 'breakpoint_theme_test.wide' => array( - 'label' => 'wide', - 'mediaQuery' => '(min-width: 851px)', - 'weight' => 2, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.wide', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.wide', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), + 'breakpoint_theme_test.narrow' => array( + 'label' => 'narrow', + 'mediaQuery' => '(min-width: 560px)', + 'weight' => 2, + 'multipliers' => array( + '1x', ), - 'breakpoint_theme_test.tv' => array( - 'label' => 'tv', - 'mediaQuery' => 'only screen and (min-width: 3456px)', - 'weight' => 3, - 'multipliers' => array( - '1x', - ), - 'provider' => 'breakpoint_theme_test', - 'id' => 'breakpoint_theme_test.tv', - 'group' => 'breakpoint_theme_test', - 'class' => 'Drupal\\breakpoint\\Breakpoint', + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.narrow', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), + 'breakpoint_theme_test.mobile' => array( + 'label' => 'mobile', + 'mediaQuery' => '(min-width: 0px)', + 'weight' => 3, + 'multipliers' => array( + '1x', ), + 'provider' => 'breakpoint_theme_test', + 'id' => 'breakpoint_theme_test.mobile', + 'group' => 'breakpoint_theme_test', + 'class' => 'Drupal\\breakpoint\\Breakpoint', + ), ); $breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup('breakpoint_theme_test'); @@ -101,7 +101,7 @@ public function testCustomBreakpointGroups () { 'breakpoint_theme_test.group2.narrow' => array( 'label' => 'narrow', 'mediaQuery' => '(min-width: 560px)', - 'weight' => 1, + 'weight' => 2, 'multipliers' => array( '1x', '2x', @@ -114,7 +114,7 @@ public function testCustomBreakpointGroups () { 'breakpoint_theme_test.group2.wide' => array( 'label' => 'wide', 'mediaQuery' => '(min-width: 851px)', - 'weight' => 2, + 'weight' => 1, 'multipliers' => array( '1x', '2x', @@ -127,7 +127,7 @@ public function testCustomBreakpointGroups () { 'breakpoint_module_test.breakpoint_theme_test.group2.tv' => array( 'label' => 'tv', 'mediaQuery' => '(min-width: 6000px)', - 'weight' => 3, + 'weight' => 0, 'multipliers' => array( '1x', ), @@ -152,7 +152,7 @@ public function testModuleBreakpoints() { 'breakpoint_module_test.mobile' => array( 'label' => 'mobile', 'mediaQuery' => '(min-width: 0px)', - 'weight' => 0, + 'weight' => 1, 'multipliers' => array( '1x', ), @@ -164,7 +164,7 @@ public function testModuleBreakpoints() { 'breakpoint_module_test.standard' => array( 'label' => 'standard', 'mediaQuery' => '(min-width: 560px)', - 'weight' => 1, + 'weight' => 0, 'multipliers' => array( '1x', '2x', diff --git a/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml b/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml index 0c24709..e1edeb2 100644 --- a/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml +++ b/core/modules/breakpoint/tests/modules/breakpoint_module_test/breakpoint_module_test.breakpoints.yml @@ -1,12 +1,12 @@ breakpoint_module_test.mobile: label: mobile mediaQuery: '(min-width: 0px)' - weight: 0 + weight: 1 # Don't include multipliers. A 1x multiplier this will be enforced by default. breakpoint_module_test.standard: label: standard mediaQuery: '(min-width: 560px)' - weight: 1 + weight: 0 # Don't include a 1x multiplier this will be enforced by default. multipliers: - 2x @@ -15,7 +15,7 @@ breakpoint_module_test.standard: breakpoint_module_test.breakpoint_theme_test.group2.tv: label: tv mediaQuery: '(min-width: 6000px)' - weight: 3 + weight: 0 multipliers: - 1x group: breakpoint_theme_test.group2 diff --git a/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php b/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php index f4b2a0b..fea2d8f 100644 --- a/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php +++ b/core/modules/breakpoint/tests/src/Unit/BreakpointTest.php @@ -87,9 +87,9 @@ public function testGetWeight() { * @covers ::getMediaQuery */ public function testGetMediaQuery() { - $this->pluginDefinition['mediaQuery'] = 'only screen and (min-width: 3456px)'; + $this->pluginDefinition['mediaQuery'] = 'only screen and (min-width: 1220px)'; $this->setupBreakpoint(); - $this->assertEquals('only screen and (min-width: 3456px)', $this->breakpoint->getMediaQuery()); + $this->assertEquals('only screen and (min-width: 1220px)', $this->breakpoint->getMediaQuery()); } /** diff --git a/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml b/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml index 6d34ec7..3e565e1 100644 --- a/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml +++ b/core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.breakpoints.yml @@ -1,32 +1,32 @@ breakpoint_theme_test.mobile: label: mobile mediaQuery: '(min-width: 0px)' - weight: 0 + weight: 3 multipliers: - 1x breakpoint_theme_test.narrow: label: narrow mediaQuery: '(min-width: 560px)' - weight: 1 + weight: 2 multipliers: - 1x # Out of order breakpoint to test sorting. breakpoint_theme_test.tv: label: tv - mediaQuery: 'only screen and (min-width: 3456px)' - weight: 3 + mediaQuery: 'only screen and (min-width: 1220px)' + weight: 0 multipliers: - 1x breakpoint_theme_test.wide: label: wide mediaQuery: '(min-width: 851px)' - weight: 2 + weight: 1 multipliers: - 1x breakpoint_theme_test.group2.narrow: label: narrow mediaQuery: '(min-width: 560px)' - weight: 1 + weight: 2 multipliers: - 1x - 2x @@ -34,7 +34,7 @@ breakpoint_theme_test.group2.narrow: breakpoint_theme_test.group2.wide: label: wide mediaQuery: '(min-width: 851px)' - weight: 2 + weight: 1 multipliers: - 1x - 2x diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationListUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationListUiTest.php index 999ebb5..d59bd7f 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationListUiTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationListUiTest.php @@ -363,13 +363,13 @@ public function doResponsiveImageListTest() { $edit['label'] = $this->randomMachineName(); $edit['id'] = strtolower($edit['label']); - $this->drupalPostForm('admin/config/media/responsive-image-mapping/add', $edit, t('Save')); - $this->assertRaw(t('Responsive image mapping %label saved.', array('%label' => $edit['label']))); + $this->drupalPostForm('admin/config/media/responsive-image-style/add', $edit, t('Save')); + $this->assertRaw(t('Responsive image style %label saved.', array('%label' => $edit['label']))); - // Get the responsive image mapping listing. - $this->drupalGet('admin/config/media/responsive-image-mapping'); + // Get the responsive image style listing. + $this->drupalGet('admin/config/media/responsive-image-style'); - $translate_link = 'admin/config/media/responsive-image-mapping/' . $edit['id'] . '/translate'; + $translate_link = 'admin/config/media/responsive-image-style/' . $edit['id'] . '/translate'; // Test if the link to translate the style is on the page. $this->assertLinkByHref($translate_link); 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 f953796..fb3b9a0 100644 --- a/core/modules/responsive_image/config/schema/responsive_image.schema.yml +++ b/core/modules/responsive_image/config/schema/responsive_image.schema.yml @@ -1,8 +1,8 @@ # Schema for the configuration files of the Responsive Image module. -responsive_image.mappings.*: +responsive_image.styles.*: type: config_entity - label: 'Responsive image mapping' + label: 'Responsive image style' mapping: id: type: string @@ -10,33 +10,57 @@ responsive_image.mappings.*: label: type: label label: 'Label' - mappings: + image_style_mappings: type: sequence - label: 'Mappings' + label: 'Image style mappings' sequence: - type: mapping - label: 'Mapping' + label: 'Image style mapping' mapping: + # Image mapping type. Either 'sizes' (using the 'sizes' attribute) + # or 'image_style' (using a single image style to map to this + # breakpoint). + image_mapping_type: + type: string + label: 'Responsive image mapping type' + image_mapping: + type: responsive_image.image_mapping_type.[%parent.image_mapping_type] breakpoint_id: type: string label: 'Breakpoint ID' multiplier: type: string label: 'Multiplier' - image_style: - type: string - label: 'Image style' - breakpointGroup: + breakpoint_group: type: string label: 'Breakpoint group' +responsive_image.image_mapping_type.image_style: + type: string + label: 'Image style' + +responsive_image.image_mapping_type.sizes: + type: mapping + mapping: + # The value for the sizes attribute as described in the spec: + # http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-sizes + sizes: + type: string + label: 'Sizes attribute' + sizes_image_styles: + type: sequence + label: 'Image styles to be used when using the ''sizes'' attribute' + sequence: + - type: string + label: 'Image style' + field.formatter.settings.responsive_image: type: mapping label: 'Responsive image list format settings' mapping: - responsive_image_mapping: + responsive_image_style: type: string - label: 'Responsive image mapping' + label: 'Responsive image style' fallback_image_style: type: string label: 'Fallback image style' diff --git a/core/modules/responsive_image/responsive_image.info.yml b/core/modules/responsive_image/responsive_image.info.yml index dec6fee..00304ef 100644 --- a/core/modules/responsive_image/responsive_image.info.yml +++ b/core/modules/responsive_image/responsive_image.info.yml @@ -7,4 +7,4 @@ core: 8.x dependencies: - breakpoint - image -configure: entity.responsive_image_mapping.collection +configure: entity.responsive_image_style.collection diff --git a/core/modules/responsive_image/responsive_image.links.action.yml b/core/modules/responsive_image/responsive_image.links.action.yml index 6308cd1..349eea4 100644 --- a/core/modules/responsive_image/responsive_image.links.action.yml +++ b/core/modules/responsive_image/responsive_image.links.action.yml @@ -1,5 +1,5 @@ -responsive_image.mapping_page_add: - route_name: responsive_image.mapping_page_add - title: 'Add responsive image mapping' +responsive_image.style_page_add: + route_name: responsive_image.style_page_add + title: 'Add responsive image style' appears_on: - - entity.responsive_image_mapping.collection + - entity.responsive_image_style.collection diff --git a/core/modules/responsive_image/responsive_image.links.menu.yml b/core/modules/responsive_image/responsive_image.links.menu.yml index c72d4ee..e361e25 100644 --- a/core/modules/responsive_image/responsive_image.links.menu.yml +++ b/core/modules/responsive_image/responsive_image.links.menu.yml @@ -1,6 +1,6 @@ -entity.responsive_image_mapping.collection: - title: 'Responsive image mappings' - description: 'Manage responsive image mappings' +entity.responsive_image_style.collection: + title: 'Responsive image styles' + description: 'Manage responsive image styles' weight: 10 - route_name: entity.responsive_image_mapping.collection + route_name: entity.responsive_image_style.collection parent: system.admin_config_media diff --git a/core/modules/responsive_image/responsive_image.links.task.yml b/core/modules/responsive_image/responsive_image.links.task.yml index 602182e..d5e69d6 100644 --- a/core/modules/responsive_image/responsive_image.links.task.yml +++ b/core/modules/responsive_image/responsive_image.links.task.yml @@ -1,5 +1,5 @@ -entity.responsive_image_mapping.edit_form: +entity.responsive_image_style.edit_form: title: Edit - route_name: entity.responsive_image_mapping.edit_form - base_route: entity.responsive_image_mapping.edit_form + route_name: entity.responsive_image_style.edit_form + base_route: entity.responsive_image_style.edit_form weight: -10 diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 7a48c0d..b4731f9 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -9,8 +9,12 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Routing\RouteMatchInterface; use \Drupal\Core\Template\Attribute; +use Drupal\image\Entity\ImageStyle; use Drupal\Core\Url; -use Drupal\responsive_image\Entity\ResponsiveImageMapping; +use Drupal\responsive_image\Entity\ResponsiveImageStyle; +use Drupal\Core\Image\ImageInterface; +use Drupal\Component\Utility\String; +use Drupal\breakpoint\BreakpointInterface; /** * The machine name for the empty image breakpoint image style option. @@ -28,15 +32,15 @@ function responsive_image_help($route_name, RouteMatchInterface $route_match) { $output .= '

' . t('The Responsive Image module provides an image formatter and breakpoint mappings to output responsive images using the HTML5 picture tag. For more information, see the online documentation for the Responsive Image module.', array( '!responsive_image' => 'https://drupal.org/documentation/modules/responsive_image')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; - $output .= '
' . t('Defining responsive image mappings') . '
'; - $output .= '
' . t('By creating responsive image mappings you define the image styles that are being used to output images at certain breakpoints. On the Responsive image mappings page, click Add responsive image mapping to create a new mapping. First chose a label and a breakpoint group and click Save. After that you can choose the image styles that will be used for each breakpoint. Image styles can be defined on the Image styles page that is provided by the Image module. Breakpoints are defined in the configuration files of the theme. See the help page of the Breakpoint module for more information.', array('!responsive_image_mapping' => \Drupal::url('entity.responsive_image_mapping.collection'), '!image_styles' => \Drupal::url('entity.image_style.collection'),'!image_help' => \Drupal::url('help.page', array('name' => 'image')), '!breakpoint_help' => \Drupal::url('help.page', array('name' => 'breakpoint')))) . '
'; - $output .= '
' . t('Using responsive image mappings in Image fields') . '
'; - $output .= '
' . t('After defining responsive image mappings, you can use them in the display settings for your Image fields, so that the site displays responsive images using the HTML5 picture tag. Open the Manage display page for the entity type (content type, taxonomy vocabulary, etc.) that the Image field is attached to. Choose the format Responsive image, click the Edit icon, and select one of the responsive image mappings that you have created. For general information on how to manage fields and their display see the Field UI module help page. For background information about entities and fields see the Field module help page.', array('!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')),'!field_help' => \Drupal::url('help.page', array('name' => 'field')))) . '
'; + $output .= '
' . t('Defining responsive image styles') . '
'; + $output .= '
' . t('By creating responsive image styles you define the image styles that are being used to output images at certain breakpoints. On the Responsive image styles page, click Add responsive image style to create a new style. First chose a label and a breakpoint group and click Save. After that you can choose the image styles that will be used for each breakpoint. Image styles can be defined on the Image styles page that is provided by the Image module. Breakpoints are defined in the configuration files of the theme. See the help page of the Breakpoint module for more information.', array('!responsive_image_style' => \Drupal::url('entity.responsive_image_style.collection'), '!image_styles' => \Drupal::url('entity.image_style.collection'),'!image_help' => \Drupal::url('help.page', array('name' => 'image')), '!breakpoint_help' => \Drupal::url('help.page', array('name' => 'breakpoint')))) . '
'; + $output .= '
' . t('Using responsive image styles in Image fields') . '
'; + $output .= '
' . t('After defining responsive image styles, you can use them in the display settings for your Image fields, so that the site displays responsive images using the HTML5 picture tag. Open the Manage display page for the entity type (content type, taxonomy vocabulary, etc.) that the Image field is attached to. Choose the format Responsive image, click the Edit icon, and select one of the responsive image styles that you have created. For general information on how to manage fields and their display see the Field UI module help page. For background information about entities and fields see the Field module help page.', array('!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')),'!field_help' => \Drupal::url('help.page', array('name' => 'field')))) . '
'; $output .= '
'; break; - case 'entity.responsive_image_mapping.collection': - $output .= '

' . t('A responsive image mapping associates an image style with each breakpoint defined by your theme.') . '

'; + case 'entity.responsive_image_style.collection': + $output .= '

' . t('A responsive image style associates an image style with each breakpoint defined by your theme.') . '

'; break; } @@ -49,19 +53,19 @@ function responsive_image_help($route_name, RouteMatchInterface $route_match) { function responsive_image_menu() { $items = array(); - $items['admin/config/media/responsive-image-mapping'] = array( - 'title' => 'Responsive image mappings', - 'description' => 'Manage responsive image mappings', + $items['admin/config/media/responsive-image-style'] = array( + 'title' => 'Responsive image styles', + 'description' => 'Manage responsive image styles', 'weight' => 10, - 'route_name' => 'entity.responsive_image_mapping.collection', + 'route_name' => 'entity.responsive_image_style.collection', ); - $items['admin/config/media/responsive-image-mapping/%responsive_image_mapping'] = array( - 'title' => 'Edit responsive image mapping', - 'route_name' => 'entity.responsive_image_mapping.edit_form', + $items['admin/config/media/responsive-image-style/%responsive_image_style'] = array( + 'title' => 'Edit responsive image style', + 'route_name' => 'entity.responsive_image_style.edit_form', ); - $items['admin/config/media/responsive-image-mapping/%responsive_image_mapping/duplicate'] = array( - 'title' => 'Duplicate responsive image mapping', - 'route_name' => 'entity.responsive_image_mapping.duplicate_form', + $items['admin/config/media/responsive-image-style/%responsive_image_style/duplicate'] = array( + 'title' => 'Duplicate responsive image style', + 'route_name' => 'entity.responsive_image_style.duplicate_form', ); return $items; @@ -81,28 +85,18 @@ function responsive_image_theme() { 'alt' => '', 'title' => NULL, 'attributes' => array(), - 'mapping_id' => array(), + 'responsive_image_style_id' => array(), ), - 'function' => 'theme_responsive_image', ), 'responsive_image_formatter' => array( 'variables' => array( 'item' => NULL, 'url' => NULL, 'image_style' => NULL, - 'mapping_id' => array(), + 'responsive_image_style_id' => array(), ), 'function' => 'theme_responsive_image_formatter', ), - 'responsive_image_source' => array( - 'variables' => array( - 'src' => NULL, - 'srcset' => NULL, - 'dimensions' => NULL, - 'media' => NULL, - ), - 'function' => 'theme_responsive_image_source', - ), ); } @@ -113,14 +107,14 @@ function responsive_image_theme() { * An associative array containing: * - item: An ImageItem object. * - image_style: An optional image style. - * - path: An optional array containing the link 'path' and link 'options'. - * - mapping_id: The ID of the responsive image mapping. + * - responsive_image_style_id: The ID of the responsive image style. + * - url: An optional \Drupal\Core\Url object. * * @ingroup themeable */ function theme_responsive_image_formatter($variables) { $item = $variables['item']; - if (!isset($variables['mapping_id']) || empty($variables['mapping_id'])) { + if (!isset($variables['responsive_image_style_id']) || empty($variables['responsive_image_style_id'])) { $image_formatter = array( '#theme' => 'image_formatter', '#item' => $item, @@ -135,7 +129,7 @@ function theme_responsive_image_formatter($variables) { '#width' => $item->width, '#height' => $item->height, '#style_name' => $variables['image_style'], - '#mapping_id' => $variables['mapping_id'], + '#responsive_image_style_id' => $variables['responsive_image_style_id'], ); if (isset($item->uri)) { $responsive_image['#uri'] = $item->uri; @@ -148,19 +142,17 @@ function theme_responsive_image_formatter($variables) { if (Unicode::strlen($item->title) != 0) { $responsive_image['#title'] = $item->title; } - // @todo Add support for route names. - if (isset($variables['path']['path'])) { - $path = $variables['path']['path']; - $options = isset($variables['path']['options']) ? $variables['path']['options'] : array(); - $options['html'] = TRUE; - return \Drupal::l($responsive_image, Url::fromUri($path, $options)); + if (isset($variables['url'])) { + return \Drupal::l($responsive_image, $variables['url']->setOption('html', TRUE)); } return drupal_render($responsive_image); } /** - * Returns HTML for a responsive image. + * Prepares variables for a responsive image. + * + * Default template: responsive-image.html.twig. * * @param $variables * An associative array containing: @@ -171,12 +163,13 @@ function theme_responsive_image_formatter($variables) { * - alt: The alternative text for text-based browsers. * - 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. * - style_name: The name of the style to be used as a fallback image. - * - mapping_id: The ID of the responsive image mapping. + * - responsive_image_style_id: The ID of the responsive image style. * * @ingroup themeable */ -function theme_responsive_image($variables) { +function template_preprocess_responsive_image(&$variables) { // Make sure that width and height are proper values // If they exists we'll output them // @see http://www.w3.org/community/respimg/2012/06/18/florians-compromise/ @@ -189,114 +182,235 @@ function theme_responsive_image($variables) { unset($variables['height']); } - $sources = array(); - - // Fallback image, output as source with media query. - $sources[] = array( - 'src' => _responsive_image_image_style_url($variables['style_name'], $variables['uri']), - 'dimensions' => responsive_image_get_image_dimensions($variables), - ); - $responsive_image_mapping = ResponsiveImageMapping::load($variables['mapping_id']); + $image = \Drupal::service('image.factory')->get($variables['uri']); + $responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']); // All breakpoints and multipliers. - $breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup($responsive_image_mapping->getBreakpointGroup()); - foreach ($responsive_image_mapping->getKeyedMappings() as $breakpoint_id => $multipliers) { + $breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup($responsive_image_style->getBreakpointGroup()); + foreach ($responsive_image_style->getKeyedImageStyleMappings() 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; - } - - // 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']; - } - $sources[] = array( - 'srcset' => implode(', ', $srcset), - 'dimensions' => responsive_image_get_image_dimensions($new_sources[0]), - 'media' => $breakpoint->getMediaQuery(), - ); - } + $variables['sources'][] = responsive_image_build_source_attributes($image, $variables, $breakpoints[$breakpoint_id], $multipliers); } } - - if (!empty($sources)) { - $output = array(); - $output[] = ''; - - // 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']; - } - $output[] = drupal_render($responsive_image_source); + // Prepare the fallback image. Use srcset in the fallback image to avoid + // unnecessary preloading of images in older browsers. See + // http://scottjehl.github.io/picturefill/#using-picture and + // http://scottjehl.github.io/picturefill/#gotchas for more information. + $variables['img_element'] = array( + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => _responsive_image_image_style_url($variables['style_name'], $image->getSource()), + ), + ), + ); + foreach (array('alt', 'title', 'attributes') as $key) { + if (isset($variables[$key])) { + $variables['img_element']["#$key"] = $variables[$key]; + unset($variables[$key]); } - - $output[] = ''; - return SafeMarkup::set(implode("\n", $output)); } } /** - * Returns HTML for a source tag. + * Helper function for template_preprocess_responsive_image(). + * + * Builds an array of attributes for tags to be used in a + * tag. In other words, this function provides the attributes for each + * tag in a tag. + * + * In a responsive image style, each breakpoint has an image style mapping for + * each of its multipliers. An image style mapping can be either of two types: + * 'sizes' (meaning it will output a tag with the 'sizes' attribute) or + * 'image_style' (meaning it will output a tag based on the selected + * image style for this breakpoint and multiplier). A responsive image style + * can contain image style mappings of mixed types (both 'image_style' and + * 'sizes'). For example: + * @code + * $responsive_img_style = ResponsiveImageStyle::create(array( + * 'id' => 'style_one', + * 'label' => 'Style One', + * 'breakpoint_group' => 'responsive_image_test_module', + * )); + * $responsive_img_style->addImageStyleMapping('responsive_image_test_module.mobile', '1x', array( + * 'image_mapping_type' => 'image_style', + * 'image_mapping' => 'thumbnail', + * )) + * ->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array( + * 'image_mapping_type' => 'sizes', + * 'image_mapping' => array( + * 'sizes' => '(min-width: 700px) 700px, 100vw', + * 'sizes_image_styles' => array( + * 'large' => 'large', + * 'medium' => 'medium', + * ), + * ), + * )) + * ->save(); + * @endcode + * The above responsive image style will result in a tag like this: + * @code + * + * + * + * + * + * @endcode + * + * When all the images in the 'srcset' attribute of a tag have the same + * MIME type, the source tag will get a 'mime-type' attribute as well. This way + * we can gain some front-end performance because browsers can select which + * image ( tag) to load based on the MIME types they support (which, for + * instance, can be beneficial for browsers supporting WebP). + * For example: + * A tag can contain multiple images: + * @code + * + * @endcode + * In the above example we can add the 'mime-type' attribute ('image/jpeg') + * since all images in the 'srcset' attribute of the tag have the same + * MIME type. + * If a tag were to look like this: + * @code + * + * @endcode + * We can't add the 'mime-type' attribute ('image/jpeg' vs 'image/webp'). So in + * order to add the 'mime-type' attribute to the tag all images in the + * 'srcset' attribute of the tag need to be of the same MIME type. This + * way, a tag could look like this: + * @code + * + * + * + * + * + * @endcode + * This way a browser can decide which tag is preferred based on the + * MIME type. In other words, the MIME types of all images in one tag + * need to be the same in order to set the 'mime-type' attribute but not all + * MIME types within the tag need to be the same. + * + * For image style mappings of the type 'sizes', a width descriptor is added to + * each source. For example: + * @code + * + * @endcode + * The width descriptor here is "100w". This way the browser knows this image is + * 100px wide without having to load it. According to the spec, a multiplier can + * not be present if a width descriptor is. + * For example: + * Valid: + * @code + * + * @endcode + * Invalid: + * @code + * + * @endcode + * + * Note: Since the specs do not allow width descriptors and multipliers combined + * inside one 'srcset' attribute, we either have to use something like + * @code + * + * @endcode + * to support multipliers or + * @code + * + * @endcode + * to support the 'sizes' attribute. + * + * In theory people could add an image style mapping for the same breakpoint + * (but different multiplier) so the array contains an entry for breakpointA.1x + * and breakpointA.2x. If we would output those we will end up with something + * like + * @code + * + * @endcode + * which is illegal. So the solution is to merge both arrays into one and + * disregard the multiplier. Which, in this case, would output + * @code + * + * @endcode + * See http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#image-candidate-string + * for further information. * + * @param \Drupal\Core\Image\ImageInterface $image + * The image to build the tags for. * @param array $variables - * An associative array containing: - * - media: The media query to use. - * - srcset: The srcset containing 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). + * An array with the following keys: + * - responsive_image_style_id: The \Drupal\responsive_image\Entity\ResponsiveImageStyle + * ID. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + * @param \Drupal\breakpoint\BreakpointInterface $breakpoint + * The breakpoint for this source tag. + * @param array $multipliers + * An array with multipliers as keys and image style mappings as values. * - * @ingroup themeable + * @return \Drupal\Core\Template\Attribute[] + * An array of attributes for the source tag. */ -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[] = ''; +function responsive_image_build_source_attributes(ImageInterface $image, array $variables, BreakpointInterface $breakpoint, array $multipliers) { + $width = isset($variables['width']) && !empty($variables['width']) ? $variables['width'] : $image->getWidth(); + $height = isset($variables['height']) && !empty($variables['height']) ? $variables['height'] : $image->getHeight(); + $extension = pathinfo($image->getSource(), PATHINFO_EXTENSION); + $sizes = array(); + $srcset = array(); + $derivative_mime_types = array(); + foreach ($multipliers as $multiplier => $image_style_mapping) { + switch ($image_style_mapping['image_mapping_type']) { + // Create a tag with the 'sizes' attribute. + case 'sizes': + // Loop through the image styles for this breakpoint and multiplier. + foreach ($image_style_mapping['image_mapping']['sizes_image_styles'] as $image_style_name) { + // Get the dimensions. + $dimensions = responsive_image_get_image_dimensions($image_style_name, array('width' => $width, 'height' => $height)); + // Get MIME type. + $derivative_mime_type = responsive_image_get_mime_type($image_style_name, $extension); + $derivative_mime_types[] = $derivative_mime_type; + + // Add the image source with its width descriptor. When a width + // descriptor is used in a srcset, we can't add a multiplier to + // it. Because of this, the image styles for all multipliers of + // this breakpoint should be merged into one srcset and the sizes + // attribute should be merged as well. + if (is_null($dimensions['width'])) { + throw new \LogicException(String::format('Could not determine image width for @file using image style with ID: @image_style_name. This image style can not be used for a responsive image style mapping using the \'sizes\' attribute.', array('@file' => $image->getSource(), '@image_style_name' => $image_style_name))); + } + $srcset[floatval($dimensions['width'])] = file_create_url(_responsive_image_image_style_url($image_style_name, $image->getSource())) . ' ' . $dimensions['width'] . 'w'; + $sizes = array_merge(explode(',', $image_style_mapping['image_mapping']['sizes']), $sizes); + } + break; + + case 'image_style': + // Get MIME type. + $derivative_mime_type = responsive_image_get_mime_type($image_style_mapping['image_mapping'], $extension); + $derivative_mime_types[] = $derivative_mime_type; + // Add the image source with its multiplier. + $srcset[floatval(Unicode::substr($multiplier, 0, -1))] = file_create_url(_responsive_image_image_style_url($image_style_mapping['image_mapping'], $image->getSource())) . ' ' . $multiplier; + break; } } - else { - $output[] = ''; - $output[] = ''; + ksort($srcset); + $source_attributes = new \Drupal\Core\Template\Attribute(array( + 'srcset' => implode(', ', array_unique($srcset)), + 'media' => $breakpoint->getMediaQuery(), + )); + if (count(array_unique($derivative_mime_types)) == 1) { + $source_attributes->setAttribute('type', $derivative_mime_types[0]); } - return implode("\n", $output); + if (!empty($sizes)) { + $source_attributes->setAttribute('sizes', implode(',', array_unique($sizes))); + } + return $source_attributes; } /** * Determines the dimensions of an image. * - * @param $variables + * @param string $image_style_name + * The name of the style to be used to alter the original image. + * @param array $dimensions * An associative array containing: - * - style_name: The name of the style to be used to alter the original image. * - width: The width of the source image (if known). * - height: The height of the source image (if known). * @@ -304,21 +418,16 @@ function theme_responsive_image_source($variables) { * Dimensions to be modified - an array with components width and height, in * pixels. */ -function responsive_image_get_image_dimensions($variables) { +function responsive_image_get_image_dimensions($image_style_name, array $dimensions) { // Determine the dimensions of the styled image. - $dimensions = array( - 'width' => $variables['width'], - 'height' => $variables['height'], - ); - - if ($variables['style_name'] == RESPONSIVE_IMAGE_EMPTY_IMAGE) { + if ($image_style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { $dimensions = array( 'width' => 1, 'height' => 1, ); } else { - $entity = entity_load('image_style', $variables['style_name']); + $entity = ImageStyle::load($image_style_name); if ($entity instanceof Drupal\image\Entity\ImageStyle) { $entity->transformDimensions($dimensions); } @@ -328,17 +437,38 @@ function responsive_image_get_image_dimensions($variables) { } /** + * Determines the MIME type of an image. + * + * @param string $image_style_name + * The image style that will be applied to the image. + * @param string $extension + * The original extension of the image (without the leading dot). + * + * @return string + * The MIME type of the image after the image style is applied. + */ +function responsive_image_get_mime_type($image_style_name, $extension) { + if ($image_style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { + return 'image/gif'; + } + // The MIME type guesser needs a full path, not just an extension, but the + // file doesn't have to exist. + $fake_path = 'responsive_image.' . ImageStyle::load($image_style_name)->getDerivativeExtension($extension); + return Drupal::service('file.mime_type.guesser.extension')->guess($fake_path); +} + +/** * Wrapper around image_style_url() so we can return an empty image. */ function _responsive_image_image_style_url($style_name, $path) { if ($style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { // The smallest data URI for a 1px square transparent GIF image. - return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever + return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; } - $entity = entity_load('image_style', $style_name); + $entity = ImageStyle::load($style_name); if ($entity instanceof Drupal\image\Entity\ImageStyle) { return $entity->buildUrl($path); } return file_create_url($path); } - diff --git a/core/modules/responsive_image/responsive_image.routing.yml b/core/modules/responsive_image/responsive_image.routing.yml index 2cf9926..6a00ff6 100644 --- a/core/modules/responsive_image/responsive_image.routing.yml +++ b/core/modules/responsive_image/responsive_image.routing.yml @@ -1,39 +1,39 @@ -entity.responsive_image_mapping.collection: - path: '/admin/config/media/responsive-image-mapping' +entity.responsive_image_style.collection: + path: '/admin/config/media/responsive-image-style' defaults: - _entity_list: 'responsive_image_mapping' - _title: 'Responsive image mappings' + _entity_list: 'responsive_image_style' + _title: 'Responsive image styles' requirements: _permission: 'administer responsive images' -responsive_image.mapping_page_add: - path: '/admin/config/media/responsive-image-mapping/add' +responsive_image.style_page_add: + path: '/admin/config/media/responsive-image-style/add' defaults: - _entity_form: 'responsive_image_mapping.add' - _title: 'Add responsive image mapping' + _entity_form: 'responsive_image_style.add' + _title: 'Add responsive image style' requirements: _permission: 'administer responsive images' -entity.responsive_image_mapping.edit_form: - path: '/admin/config/media/responsive-image-mapping/{responsive_image_mapping}' +entity.responsive_image_style.edit_form: + path: '/admin/config/media/responsive-image-style/{responsive_image_style}' defaults: - _entity_form: 'responsive_image_mapping.edit' - _title: 'Edit responsive image mapping' + _entity_form: 'responsive_image_style.edit' + _title: 'Edit responsive image style' requirements: _permission: 'administer responsive images' -entity.responsive_image_mapping.duplicate_form: - path: '/admin/config/media/responsive-image-mapping/{responsive_image_mapping}/duplicate' +entity.responsive_image_style.duplicate_form: + path: '/admin/config/media/responsive-image-style/{responsive_image_style}/duplicate' defaults: - _entity_form: 'responsive_image_mapping.duplicate' - _title: 'Duplicate responsive image mapping' + _entity_form: 'responsive_image_style.duplicate' + _title: 'Duplicate responsive image style' requirements: _permission: 'administer responsive images' -responsive_image.mapping_action_confirm: - path: '/admin/config/media/responsive-image-mapping/{responsive_image_mapping}/delete' +entity.responsive_image_style.delete_form: + path: '/admin/config/media/responsive-image-style/{responsive_image_style}/delete' defaults: - _entity_form: 'responsive_image_mapping.delete' + _entity_form: 'responsive_image_style.delete' _title: 'Delete' requirements: _permission: 'administer responsive images' diff --git a/core/modules/responsive_image/src/Entity/ResponsiveImageMapping.php b/core/modules/responsive_image/src/Entity/ResponsiveImageMapping.php deleted file mode 100644 index 62215d8..0000000 --- a/core/modules/responsive_image/src/Entity/ResponsiveImageMapping.php +++ /dev/null @@ -1,185 +0,0 @@ -mappings as &$mapping) { - if ($mapping['breakpoint_id'] === $breakpoint_id && $mapping['multiplier'] === $multiplier) { - $mapping['image_style'] = $image_style; - return $this; - } - } - $this->mappings[] = array( - 'breakpoint_id' => $breakpoint_id, - 'multiplier' => $multiplier, - 'image_style' => $image_style, - ); - $this->keyedMappings = NULL; - return $this; - } - - /** - * {@inheritdoc} - */ - public function hasMappings() { - return !empty($this->mappings); - } - - /** - * {@inheritdoc} - */ - public function getKeyedMappings() { - if (!$this->keyedMappings) { - $this->keyedMappings = array(); - foreach($this->mappings as $mapping) { - $this->keyedMappings[$mapping['breakpoint_id']][$mapping['multiplier']] = $mapping['image_style']; - } - } - return $this->keyedMappings; - } - - /** - * {@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'); - } - - /** - * {@inheritdoc} - */ - public function setBreakpointGroup($breakpoint_group) { - // If the breakpoint group is changed then the mappings are invalid. - if ($breakpoint_group !== $this->breakpointGroup) { - $this->removeMappings(); - } - $this->set('breakpointGroup', $breakpoint_group); - return $this; - } - - /** - * {@inheritdoc} - */ - public function getBreakpointGroup() { - return $this->get('breakpointGroup'); - } - - /** - * {@inheritdoc} - */ - public function removeMappings() { - $this->mappings = array(); - $this->keyedMappings = NULL; - return $this; - } - - /** - * {@inheritdoc} - */ - public function calculateDependencies() { - parent::calculateDependencies(); - $providers = \Drupal::service('breakpoint.manager')->getGroupProviders($this->breakpointGroup); - foreach ($providers as $provider => $type) { - $this->addDependency($type, $provider); - } - return $this->dependencies; - } - -} diff --git a/core/modules/responsive_image/src/Entity/ResponsiveImageStyle.php b/core/modules/responsive_image/src/Entity/ResponsiveImageStyle.php new file mode 100644 index 0000000..aee5a70 --- /dev/null +++ b/core/modules/responsive_image/src/Entity/ResponsiveImageStyle.php @@ -0,0 +1,224 @@ +image_style_mappings as &$mapping) { + if ($mapping['breakpoint_id'] === $breakpoint_id && $mapping['multiplier'] === $multiplier) { + $mapping = array( + 'breakpoint_id' => $breakpoint_id, + 'multiplier' => $multiplier, + ) + $image_style_mapping; + return $this; + } + } + $this->image_style_mappings[] = array( + 'breakpoint_id' => $breakpoint_id, + 'multiplier' => $multiplier, + ) + $image_style_mapping; + $this->keyedImageStyleMappings = NULL; + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasImageStyleMappings() { + $mappings = $this->getKeyedImageStyleMappings(); + return !empty($mappings); + } + + /** + * {@inheritdoc} + */ + public function getKeyedImageStyleMappings() { + if (!$this->keyedImageStyleMappings) { + $this->keyedImageStyleMappings = array(); + foreach($this->image_style_mappings as $mapping) { + if (!static::isEmptyImageStyleMapping($mapping)) { + $this->keyedImageStyleMappings[$mapping['breakpoint_id']][$mapping['multiplier']] = $mapping; + } + } + } + return $this->keyedImageStyleMappings; + } + + /** + * {@inheritdoc} + */ + public function getImageStyleMappings() { + return $this->image_style_mappings; + } + + /** + * {@inheritdoc} + */ + public function setBreakpointGroup($breakpoint_group) { + // If the breakpoint group is changed then the image style mappings are + // invalid. + if ($breakpoint_group !== $this->breakpoint_group) { + $this->removeImageStyleMappings(); + } + $this->breakpoint_group = $breakpoint_group; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getBreakpointGroup() { + return $this->breakpoint_group; + } + + /** + * {@inheritdoc} + */ + public function removeImageStyleMappings() { + $this->image_style_mappings = array(); + $this->keyedImageStyleMappings = NULL; + return $this; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + $providers = \Drupal::service('breakpoint.manager')->getGroupProviders($this->breakpoint_group); + foreach ($providers as $provider => $type) { + $this->addDependency($type, $provider); + } + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public static function isEmptyImageStyleMapping(array $image_style_mapping) { + if (!empty($image_style_mapping)) { + switch ($image_style_mapping['image_mapping_type']) { + case 'sizes': + // The image style mapping must have a sizes attribute defined and one + // or more image styles selected. + if ($image_style_mapping['image_mapping']['sizes'] && $image_style_mapping['image_mapping']['sizes_image_styles']) { + return FALSE; + } + break; + case 'image_style': + // The image style mapping must have an image style selected. + if ($image_style_mapping['image_mapping']) { + return FALSE; + } + break; + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getImageStyleMapping($breakpoint_id, $multiplier) { + $map = $this->getKeyedImageStyleMappings(); + 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 ed6bcf0..0cf8d8a 100644 --- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php +++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php @@ -16,6 +16,8 @@ use Drupal\Core\Url; use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\responsive_image\Entity\ResponsiveImageStyle; +use Drupal\image\Entity\ImageStyle; /** * Plugin for responsive image formatter. @@ -33,7 +35,7 @@ class ResponsiveImageFormatter extends ImageFormatterBase implements ContainerFa /** * @var EntityStorageInterface */ - protected $responsiveImageMappingStorage; + protected $responsiveImageStyleStorage; /** * Constructs a ResponsiveImageFormatter object. @@ -52,13 +54,13 @@ class ResponsiveImageFormatter extends ImageFormatterBase implements ContainerFa * The view mode. * @param array $third_party_settings * Any third party settings. - * @param \Drupal\Core\Entity\EntityStorageInterface $responsive_image_mapping_storage - * The responsive image mapping storage. + * @param \Drupal\Core\Entity\EntityStorageInterface $responsive_image_style_storage + * The responsive image style storage. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityStorageInterface $responsive_image_mapping_storage) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityStorageInterface $responsive_image_style_storage) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); - $this->responsiveImageMappingStorage = $responsive_image_mapping_storage; + $this->responsiveImageStyleStorage = $responsive_image_style_storage; } /** @@ -73,7 +75,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['label'], $configuration['view_mode'], $configuration['third_party_settings'], - $container->get('entity.manager')->getStorage('responsive_image_mapping') + $container->get('entity.manager')->getStorage('responsive_image_style') ); } @@ -82,7 +84,7 @@ public static function create(ContainerInterface $container, array $configuratio */ public static function defaultSettings() { return array( - 'responsive_image_mapping' => '', + 'responsive_image_style' => '', 'fallback_image_style' => '', 'image_link' => '', ) + parent::defaultSettings(); @@ -93,19 +95,19 @@ public static function defaultSettings() { */ public function settingsForm(array $form, FormStateInterface $form_state) { $responsive_image_options = array(); - $responsive_image_mappings = $this->responsiveImageMappingStorage->loadMultiple(); - if ($responsive_image_mappings && !empty($responsive_image_mappings)) { - foreach ($responsive_image_mappings as $machine_name => $responsive_image_mapping) { - if ($responsive_image_mapping->hasMappings()) { - $responsive_image_options[$machine_name] = $responsive_image_mapping->label(); + $responsive_image_styles = $this->responsiveImageStyleStorage->loadMultiple(); + if ($responsive_image_styles && !empty($responsive_image_styles)) { + foreach ($responsive_image_styles as $machine_name => $responsive_image_style) { + if ($responsive_image_style->hasImageStyleMappings()) { + $responsive_image_options[$machine_name] = $responsive_image_style->label(); } } } - $elements['responsive_image_mapping'] = array( - '#title' => t('Responsive image mapping'), + $elements['responsive_image_style'] = array( + '#title' => t('Responsive image style'), '#type' => 'select', - '#default_value' => $this->getSetting('responsive_image_mapping'), + '#default_value' => $this->getSetting('responsive_image_style'), '#required' => TRUE, '#options' => $responsive_image_options, ); @@ -140,9 +142,9 @@ public function settingsForm(array $form, FormStateInterface $form_state) { public function settingsSummary() { $summary = array(); - $responsive_image_mapping = $this->responsiveImageMappingStorage->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())); + $responsive_image_style = $this->responsiveImageStyleStorage->load($this->getSetting('responsive_image_style')); + if ($responsive_image_style) { + $summary[] = t('Responsive image style: @responsive_image_style', array('@responsive_image_style' => $responsive_image_style->label())); $image_styles = image_style_options(FALSE); unset($image_styles['']); @@ -163,7 +165,7 @@ public function settingsSummary() { } } else { - $summary[] = t('Select a responsive image mapping.'); + $summary[] = t('Select a responsive image style.'); } return $summary; @@ -191,28 +193,47 @@ public function viewElements(FieldItemListInterface $items) { } // Collect cache tags to be added for each item in the field. - $responsive_image_mapping = $this->responsiveImageMappingStorage->load($this->getSetting('responsive_image_mapping')); + $responsive_image_style = $this->responsiveImageStyleStorage->load($this->getSetting('responsive_image_style')); $image_styles_to_load = array(); - if ($fallback_image_style) { - $image_styles_to_load[] = $fallback_image_style; - } $cache_tags = []; - if ($responsive_image_mapping) { - $cache_tags = Cache::mergeTags($cache_tags, $responsive_image_mapping->getCacheTags()); - foreach ($responsive_image_mapping->getMappings() as $mapping) { - // First mapping found is used as fallback. - if (empty($fallback_image_style)) { - $fallback_image_style = $mapping['image_style']; + if ($responsive_image_style) { + $cache_tags = Cache::mergeTags($cache_tags, $responsive_image_style->getCacheTags()); + foreach ($responsive_image_style->getImageStyleMappings() as $image_style_mapping) { + // Only image styles of non-empty mappings should be loaded. + if (!$responsive_image_style::isEmptyImageStyleMapping($image_style_mapping)) { + + if ($image_style_mapping['image_mapping_type'] == 'image_style') { + // This mapping has one image style, add it. + $image_styles_to_load[] = $image_style_mapping['image_mapping']; + } + else { + // This mapping has multiple image styles, merge them. + $image_style_mapping['image_mapping']['sizes_image_styles'] = array_filter($image_style_mapping['image_mapping']['sizes_image_styles']); + $image_styles_to_load = array_merge($image_styles_to_load, $image_style_mapping['image_mapping']['sizes_image_styles']); + } } - $image_styles_to_load[] = $mapping['image_style']; } } - $image_styles = entity_load_multiple('image_style', $image_styles_to_load); + + // If there is a fallback image style, add it to the image styles to load. + if ($fallback_image_style) { + $image_styles_to_load[] = $fallback_image_style; + } + else { + // The element uses the first matching breakpoint (see + // http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#update-the-source-set + // points 2 and 3). Meaning the breakpoints are sorted from large to + // small. With mobile-first in mind, the fallback image should be the one + // selected for the smallest screen. + $fallback_image_style = end($image_styles_to_load); + } + $image_styles = ImageStyle::loadMultiple($image_styles_to_load); foreach ($image_styles as $image_style) { $cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags()); } foreach ($items as $delta => $item) { + // Link the element to the original file. if (isset($link_file)) { $url = Url::fromUri(file_create_url($item->entity->getFileUri())); } @@ -221,15 +242,15 @@ public function viewElements(FieldItemListInterface $items) { '#attached' => array( 'library' => array( 'core/picturefill', - ) + ), ), '#item' => $item, '#image_style' => $fallback_image_style, - '#mapping_id' => $responsive_image_mapping ? $responsive_image_mapping->id() : '', + '#responsive_image_style_id' => $responsive_image_style ? $responsive_image_style->id() : '', '#url' => $url, '#cache' => array( 'tags' => $cache_tags, - ) + ), ); } diff --git a/core/modules/responsive_image/src/ResponsiveImageMappingForm.php b/core/modules/responsive_image/src/ResponsiveImageMappingForm.php deleted file mode 100644 index 1f47865..0000000 --- a/core/modules/responsive_image/src/ResponsiveImageMappingForm.php +++ /dev/null @@ -1,169 +0,0 @@ -get('breakpoint.manager') - ); - } - - /** - * Constructs the responsive image mapping form. - * - * @param \Drupal\breakpoint\BreakpointManagerInterface $breakpoint_manager - * The breakpoint manager. - */ - public function __construct(BreakpointManagerInterface $breakpoint_manager) { - $this->breakpointManager = $breakpoint_manager; - } - - /** - * Overrides Drupal\Core\Entity\EntityForm::form(). - * - * @param array $form - * A nested array form elements comprising the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The array containing the complete form. - */ - public function form(array $form, FormStateInterface $form_state) { - if ($this->operation == 'duplicate') { - $form['#title'] = $this->t('Duplicate responsive image mapping @label', array('@label' => $this->entity->label())); - $this->entity = $this->entity->createDuplicate(); - } - if ($this->operation == 'edit') { - $form['#title'] = $this->t('Edit responsive image mapping @label', array('@label' => $this->entity->label())); - } - - /** @var \Drupal\responsive_image\ResponsiveImageMappingInterface $responsive_image_mapping */ - $responsive_image_mapping = $this->entity; - $form['label'] = array( - '#type' => 'textfield', - '#title' => $this->t('Label'), - '#maxlength' => 255, - '#default_value' => $responsive_image_mapping->label(), - '#description' => $this->t("Example: 'Hero image' or 'Author image'."), - '#required' => TRUE, - ); - $form['id'] = array( - '#type' => 'machine_name', - '#default_value' => $responsive_image_mapping->id(), - '#machine_name' => array( - 'exists' => '\Drupal\responsive_image\Entity\ResponsiveImageMapping::load', - 'source' => array('label'), - ), - '#disabled' => (bool) $responsive_image_mapping->id() && $this->operation != 'duplicate', - ); - - if ((bool) $responsive_image_mapping->id() && $this->operation != 'duplicate') { - $description = $this->t('Select a breakpoint group from the installed themes.') . ' ' . $this->t("Warning: if you change the breakpoint group you lose all your selected mappings."); - } - else { - $description = $this->t('Select a breakpoint group from the installed themes.'); - } - $form['breakpointGroup'] = array( - '#type' => 'select', - '#title' => $this->t('Breakpoint group'), - '#default_value' => $responsive_image_mapping->getBreakpointGroup(), - '#options' => $this->breakpointManager->getGroups(), - '#required' => TRUE, - '#description' => $description, - ); - - $image_styles = image_style_options(TRUE); - $image_styles[RESPONSIVE_IMAGE_EMPTY_IMAGE] = $this->t('- empty image -'); - $breakpoints = $this->breakpointManager->getBreakpointsByGroup($responsive_image_mapping->getBreakpointGroup()); - foreach ($breakpoints as $breakpoint_id => $breakpoint) { - foreach ($breakpoint->getMultipliers() as $multiplier) { - $label = $multiplier . ' ' . $breakpoint->getLabel() . ' [' . $breakpoint->getMediaQuery() . ']'; - $form['keyed_mappings'][$breakpoint_id][$multiplier] = array( - '#type' => 'select', - '#title' => $label, - '#options' => $image_styles, - '#default_value' => $responsive_image_mapping->getImageStyle($breakpoint_id, $multiplier), - '#description' => $this->t('Select an image style for this breakpoint.'), - ); - } - } - - $form['#tree'] = TRUE; - - return parent::form($form, $form_state, $responsive_image_mapping); - } - - /** - * {@inheritdoc} - */ - public function validate(array $form, FormStateInterface $form_state) { - // Only validate on edit. - if ($form_state->hasValue('keyed_mappings')) { - // Check if another breakpoint group is selected. - if ($form_state->getValue('breakpointGroup') != $form_state->getCompleteForm()['breakpointGroup']['#default_value']) { - // Remove the mappings since the breakpoint ID has changed. - $form_state->unsetValue('keyed_mappings'); - } - } - } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - /** @var \Drupal\responsive_image\ResponsiveImageMappingInterface $responsive_image_mapping */ - $responsive_image_mapping = $this->entity; - // Remove all the existing mappings and replace with submitted values. - $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); - } - } - } - $responsive_image_mapping->save(); - - $this->logger('responsive_image')->notice('Responsive image mapping @label saved.', array('@label' => $responsive_image_mapping->label())); - drupal_set_message($this->t('Responsive image mapping %label saved.', array('%label' => $responsive_image_mapping->label()))); - - // Redirect to edit form after creating a new mapping or after selecting - // another breakpoint group. - if (!$responsive_image_mapping->hasMappings()) { - $form_state->setRedirect( - 'entity.responsive_image_mapping.edit_form', - array('responsive_image_mapping' => $responsive_image_mapping->id()) - ); - } - else { - $form_state->setRedirectUrl($this->entity->urlInfo('collection')); - } - } - -} diff --git a/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php b/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php deleted file mode 100644 index fd9b28b..0000000 --- a/core/modules/responsive_image/src/ResponsiveImageMappingInterface.php +++ /dev/null @@ -1,98 +0,0 @@ -getLabel($entity); - $row['id'] = $entity->id(); - return $row + parent::buildRow($entity); - } - - /** - * {@inheritdoc} - */ - public function getDefaultOperations(EntityInterface $entity) { - $operations = parent::getDefaultOperations($entity); - $operations['duplicate'] = array( - 'title' => t('Duplicate'), - 'weight' => 15, - 'url' => $entity->urlInfo('duplicate-form'), - ); - return $operations; - } - -} diff --git a/core/modules/responsive_image/src/ResponsiveImageStyleForm.php b/core/modules/responsive_image/src/ResponsiveImageStyleForm.php new file mode 100644 index 0000000..365f7a5 --- /dev/null +++ b/core/modules/responsive_image/src/ResponsiveImageStyleForm.php @@ -0,0 +1,183 @@ +get('breakpoint.manager') + ); + } + + /** + * Constructs the responsive image style form. + * + * @param \Drupal\breakpoint\BreakpointManagerInterface $breakpoint_manager + * The breakpoint manager. + */ + public function __construct(BreakpointManagerInterface $breakpoint_manager) { + $this->breakpointManager = $breakpoint_manager; + } + + /** + * Overrides Drupal\Core\Entity\EntityForm::form(). + * + * @param array $form + * A nested array form elements comprising the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The array containing the complete form. + */ + public function form(array $form, FormStateInterface $form_state) { + if ($this->operation == 'duplicate') { + $form['#title'] = $this->t('Duplicate responsive image style @label', array('@label' => $this->entity->label())); + $this->entity = $this->entity->createDuplicate(); + } + if ($this->operation == 'edit') { + $form['#title'] = $this->t('Edit responsive image style @label', array('@label' => $this->entity->label())); + } + + /** @var \Drupal\responsive_image\ResponsiveImageStyleInterface $responsive_image_style */ + $responsive_image_style = $this->entity; + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $responsive_image_style->label(), + '#description' => $this->t("Example: 'Hero image' or 'Author image'."), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $responsive_image_style->id(), + '#machine_name' => array( + 'exists' => '\Drupal\responsive_image\Entity\ResponsiveImageStyle::load', + 'source' => array('label'), + ), + '#disabled' => (bool) $responsive_image_style->id() && $this->operation != 'duplicate', + ); + + if ((bool) $responsive_image_style->id() && $this->operation != 'duplicate') { + $description = $this->t('Select a breakpoint group from the installed themes.') . ' ' . $this->t("Warning: if you change the breakpoint group you lose all your selected image style mappings."); + } + else { + $description = $this->t('Select a breakpoint group from the installed themes.'); + } + $form['breakpoint_group'] = array( + '#type' => 'select', + '#title' => $this->t('Breakpoint group'), + '#default_value' => $responsive_image_style->getBreakpointGroup(), + '#options' => $this->breakpointManager->getGroups(), + '#required' => TRUE, + '#description' => $description, + ); + + $image_styles = image_style_options(TRUE); + $image_styles[RESPONSIVE_IMAGE_EMPTY_IMAGE] = $this->t('- empty image -'); + $breakpoints = $this->breakpointManager->getBreakpointsByGroup($responsive_image_style->getBreakpointGroup()); + foreach ($breakpoints as $breakpoint_id => $breakpoint) { + foreach ($breakpoint->getMultipliers() as $multiplier) { + $label = $multiplier . ' ' . $breakpoint->getLabel() . ' [' . $breakpoint->getMediaQuery() . ']'; + $form['keyed_styles'][$breakpoint_id][$multiplier] = array( + '#type' => 'container', + ); + $image_style_mapping = $responsive_image_style->getImageStyleMapping($breakpoint_id, $multiplier); + // @todo The image_mapping_type is only temporarily hardcoded, until + // support for the other responsive image mapping type ('sizes') is + // added in https://www.drupal.org/node/2334387. + $form['keyed_styles'][$breakpoint_id][$multiplier]['image_mapping_type'] = array( + '#type' => 'value', + '#value' => 'image_style', + ); + $form['keyed_styles'][$breakpoint_id][$multiplier]['image_mapping'] = array( + '#type' => 'select', + '#title' => $label, + '#options' => $image_styles, + '#default_value' => isset($image_style_mapping['image_mapping']) ? $image_style_mapping['image_mapping'] : array(), + '#description' => $this->t('Select an image style for this breakpoint.'), + ); + } + } + + $form['#tree'] = TRUE; + + return parent::form($form, $form_state, $responsive_image_style); + } + + /** + * {@inheritdoc} + */ + public function validate(array $form, FormStateInterface $form_state) { + // Only validate on edit. + if ($form_state->hasValue('keyed_styles')) { + // Check if another breakpoint group is selected. + if ($form_state->getValue('breakpoint_group') != $form_state->getCompleteForm()['breakpoint_group']['#default_value']) { + // Remove the image style mappings since the breakpoint ID has changed. + $form_state->unsetValue('keyed_styles'); + } + // @todo Filter 'sizes_image_styles' to a normal array in + // https://www.drupal.org/node/2334387. For an example see + // \Drupal\Core\Block\BlockBase::validateConfigurationForm(). + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /** @var \Drupal\responsive_image\ResponsiveImageStyleInterface $responsive_image_style */ + $responsive_image_style = $this->entity; + // Remove all the existing mappings and replace with submitted values. + $responsive_image_style->removeImageStyleMappings(); + if ($form_state->hasValue('keyed_styles')) { + foreach ($form_state->getValue('keyed_styles') as $breakpoint_id => $multipliers) { + foreach ($multipliers as $multiplier => $image_style_mapping) { + $responsive_image_style->addImageStyleMapping($breakpoint_id, $multiplier, $image_style_mapping); + } + } + } + $responsive_image_style->save(); + + $this->logger('responsive_image')->notice('Responsive image style @label saved.', array('@label' => $responsive_image_style->label())); + drupal_set_message($this->t('Responsive image style %label saved.', array('%label' => $responsive_image_style->label()))); + + // Redirect to edit form after creating a new responsive image style or + // after selecting another breakpoint group. + if (!$responsive_image_style->hasImageStyleMappings()) { + $form_state->setRedirect( + 'entity.responsive_image_style.edit_form', + array('responsive_image_style' => $responsive_image_style->id()) + ); + } + else { + $form_state->setRedirectUrl($this->entity->urlInfo('collection')); + } + } + +} diff --git a/core/modules/responsive_image/src/ResponsiveImageStyleInterface.php b/core/modules/responsive_image/src/ResponsiveImageStyleInterface.php new file mode 100644 index 0000000..feb3639 --- /dev/null +++ b/core/modules/responsive_image/src/ResponsiveImageStyleInterface.php @@ -0,0 +1,129 @@ +getLabel($entity); + $row['id'] = $entity->id(); + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + $operations['duplicate'] = array( + 'title' => t('Duplicate'), + 'weight' => 15, + 'url' => $entity->urlInfo('duplicate-form'), + ); + return $operations; + } + +} diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php index 5935ee0..044a85c 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageAdminUITest.php @@ -38,64 +38,76 @@ protected function setUp() { * Test responsive image administration functionality. */ public function testResponsiveImageAdmin() { - // We start without any default mappings. - $this->drupalGet('admin/config/media/responsive-image-mapping'); - $this->assertText('There is no Responsive image mapping yet.'); + // We start without any default styles. + $this->drupalGet('admin/config/media/responsive-image-style'); + $this->assertText('There is no Responsive image style yet.'); - // Add a new responsive image mapping, our breakpoint set should be selected. - $this->drupalGet('admin/config/media/responsive-image-mapping/add'); - $this->assertFieldByName('breakpointGroup', 'responsive_image_test_module'); + // Add a new responsive image style, our breakpoint set should be selected. + $this->drupalGet('admin/config/media/responsive-image-style/add'); + $this->assertFieldByName('breakpoint_group', 'responsive_image_test_module'); // Create a new group. $edit = array( - 'label' => 'Mapping One', - 'id' => 'mapping_one', - 'breakpointGroup' => 'responsive_image_test_module', + 'label' => 'Style One', + 'id' => 'style_one', + 'breakpoint_group' => 'responsive_image_test_module', ); - $this->drupalPostForm('admin/config/media/responsive-image-mapping/add', $edit, t('Save')); + $this->drupalPostForm('admin/config/media/responsive-image-style/add', $edit, t('Save')); // Check if the new group is created. $this->assertResponse(200); - $this->drupalGet('admin/config/media/responsive-image-mapping'); - $this->assertNoText('There is no Responsive image mapping yet.'); - $this->assertText('Mapping One'); - $this->assertText('mapping_one'); + $this->drupalGet('admin/config/media/responsive-image-style'); + $this->assertNoText('There is no Responsive image style yet.'); + $this->assertText('Style One'); + $this->assertText('style_one'); // Edit the group. - $this->drupalGet('admin/config/media/responsive-image-mapping/mapping_one'); - $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]', ''); - - // Save mappings for 1x variant only. + $this->drupalGet('admin/config/media/responsive-image-style/style_one'); + $this->assertFieldByName('label', 'Style One'); + $this->assertFieldByName('breakpoint_group', 'responsive_image_test_module'); + + $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_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_mapping]', ''); + } + + // Save styles 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', + 'label' => 'Style One', + 'breakpoint_group' => 'responsive_image_test_module', + 'keyed_styles[responsive_image_test_module.mobile][1x][image_mapping]' => 'thumbnail', + 'keyed_styles[responsive_image_test_module.narrow][1x][image_mapping]' => 'medium', + 'keyed_styles[responsive_image_test_module.wide][1x][image_mapping]' => '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]', ''); - - // Delete the mapping. - $this->drupalGet('admin/config/media/responsive-image-mapping/mapping_one/delete'); + $this->drupalPostForm('admin/config/media/responsive-image-style/style_one', $edit, t('Save')); + $this->drupalGet('admin/config/media/responsive-image-style/style_one'); + + // Check the style for multipliers 1x and 2x for the mobile breakpoint. + $this->assertFieldByName('keyed_styles[responsive_image_test_module.mobile][1x][image_mapping]', 'thumbnail'); + $this->assertFieldByName('keyed_styles[responsive_image_test_module.mobile][2x][image_mapping]', ''); + + // Check the style for multipliers 1x and 2x for the narrow breakpoint. + $this->assertFieldByName('keyed_styles[responsive_image_test_module.narrow][1x][image_mapping]', 'medium'); + $this->assertFieldByName('keyed_styles[responsive_image_test_module.narrow][2x][image_mapping]', ''); + + // Check the style for multipliers 1x and 2x for the wide breakpoint. + $this->assertFieldByName('keyed_styles[responsive_image_test_module.wide][1x][image_mapping]', 'large'); + $this->assertFieldByName('keyed_styles[responsive_image_test_module.wide][2x][image_mapping]', ''); + + // Delete the style. + $this->drupalGet('admin/config/media/responsive-image-style/style_one/delete'); $this->drupalPostForm(NULL, array(), t('Delete')); - $this->drupalGet('admin/config/media/responsive-image-mapping'); - $this->assertText('There is no Responsive image mapping yet.'); + $this->drupalGet('admin/config/media/responsive-image-style'); + $this->assertText('There is no Responsive image style yet.'); } } diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php index b09a9e5..c59c7ee 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php @@ -9,6 +9,9 @@ use Drupal\Component\Utility\Unicode; use Drupal\image\Tests\ImageFieldTestBase; +use Drupal\image\Entity\ImageStyle; +use Drupal\node\Entity\Node; +use Drupal\file\Entity\File; /** * Tests responsive image display formatter. @@ -20,11 +23,11 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { protected $dumpHeaders = TRUE; /** - * Responsive image mapping entity instance we test with. + * Responsive image style entity instance we test with. * - * @var \Drupal\responsive_image\Entity\ResponsiveImageMapping + * @var \Drupal\responsive_image\Entity\ResponsiveImageStyle */ - protected $responsiveImgMapping; + protected $responsiveImgStyle; /** * Modules to enable. @@ -54,27 +57,27 @@ protected function setUp() { 'administer image styles' )); $this->drupalLogin($this->adminUser); - // Add responsive image mapping. - $this->responsiveImgMapping = entity_create('responsive_image_mapping', array( - 'id' => 'mapping_one', - 'label' => 'Mapping One', - 'breakpointGroup' => 'responsive_image_test_module', + // Add responsive image style. + $this->responsiveImgStyle = entity_create('responsive_image_style', array( + 'id' => 'style_one', + 'label' => 'Style One', + 'breakpoint_group' => 'responsive_image_test_module', )); } /** - * Test responsive image formatters on node display for public files. + * Tests responsive image formatters on node display for public files. */ public function testResponsiveImageFieldFormattersPublic() { - $this->addTestMappings(); + $this->addTestImageStyleMappings(); $this->doTestResponsiveImageFieldFormatters('public'); } /** - * Test responsive image formatters on node display for private files. + * Tests responsive image formatters on node display for private files. */ public function testResponsiveImageFieldFormattersPrivate() { - $this->addTestMappings(); + $this->addTestImageStyleMappings(); // Remove access content permission from anonymous users. user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('access content' => FALSE)); $this->doTestResponsiveImageFieldFormatters('private'); @@ -84,29 +87,59 @@ public function testResponsiveImageFieldFormattersPrivate() { * Test responsive image formatters when image style is empty. */ public function testResponsiveImageFieldFormattersEmptyStyle() { - $this->addTestMappings(TRUE); + $this->addTestImageStyleMappings(TRUE); $this->doTestResponsiveImageFieldFormatters('public', TRUE); } /** - * Add mappings to the responsive image mapping entity. + * Add image style mappings to the responsive image style entity. * * @param bool $empty_styles - * If true, the mappings will get empty image styles. + * If true, the image style mappings will get empty image styles. */ - protected function addTestMappings($empty_styles = FALSE) { + protected function addTestImageStyleMappings($empty_styles = FALSE) { if ($empty_styles) { - $this->responsiveImgMapping - ->addMapping('responsive_image_test_module.mobile', '1x', '') - ->addMapping('responsive_image_test_module.narrow', '1x', '') - ->addMapping('responsive_image_test_module.wide', '1x', '') + $this->responsiveImgStyle + ->addImageStyleMapping('responsive_image_test_module.mobile', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => '', + )) + ->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width: 700px) 700px, 100vw', + 'sizes_image_styles' => array(), + ), + )) + ->addImageStyleMapping('responsive_image_test_module.wide', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => '', + )) ->save(); } else { - $this->responsiveImgMapping - ->addMapping('responsive_image_test_module.mobile', '1x', 'thumbnail') - ->addMapping('responsive_image_test_module.narrow', '1x', 'medium') - ->addMapping('responsive_image_test_module.wide', '1x', 'large') + $this->responsiveImgStyle + // Test the output of an empty image. + ->addImageStyleMapping('responsive_image_test_module.mobile', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => RESPONSIVE_IMAGE_EMPTY_IMAGE, + )) + // Test the output of the 'sizes' attribute. + ->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width: 700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + 'medium' => 'medium', + ), + ), + )) + // Test the normal output of mapping to an image style. + ->addImageStyleMapping('responsive_image_test_module.wide', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )) ->save(); } } @@ -133,7 +166,7 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = $node = $node_storage->load($nid); // Test that the default formatter is being used. - $image_uri = file_load($node->{$field_name}->target_id)->getFileUri(); + $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); $image = array( '#theme' => 'image', '#uri' => $image_uri, @@ -183,50 +216,126 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = $this->drupalLogin($this->adminUser); } - // Use the responsive image formatter with a responsive image mapping. - $display_options['settings']['responsive_image_mapping'] = 'mapping_one'; + // Use the responsive image formatter with a responsive image style. + $display_options['settings']['responsive_image_style'] = 'style_one'; $display_options['settings']['image_link'] = ''; // Also set the fallback image style. $display_options['settings']['fallback_image_style'] = 'large'; $display->setComponent($field_name, $display_options) ->save(); + // Create a derivative so at least one MIME type will be known. + $large_style = ImageStyle::load('large'); + $large_style->createDerivative($image_uri, $large_style->buildUri($image_uri)); + // Output should contain all image styles and all breakpoints. $this->drupalGet('node/' . $nid); if (!$empty_styles) { - $this->assertRaw('/styles/thumbnail/'); $this->assertRaw('/styles/medium/'); + // Make sure the IE9 workaround is present. + $this->assertRaw(''); + $this->assertRaw(''); + // Assert the empty image is present. + $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); + $this->assertRaw('/styles/medium/'); + // Assert the output of the breakpoints. + $this->assertRaw('media="(min-width: 0px)"'); + $this->assertRaw('media="(min-width: 560px)"'); + // Assert the output of the 'sizes' attribute. + $this->assertRaw('sizes="(min-width: 700px) 700px, 100vw"'); + $this->assertPattern('/media="\(min-width: 560px\)".+?sizes="\(min-width: 700px\) 700px, 100vw"/'); + $this->assertRaw('media="(min-width: 851px)"'); } $this->assertRaw('/styles/large/'); - $this->assertRaw('media="(min-width: 0px)"'); - $this->assertRaw('media="(min-width: 560px)"'); - $this->assertRaw('media="(min-width: 851px)"'); $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags')); - $this->assertTrue(in_array('config:responsive_image.mappings.mapping_one', $cache_tags)); + $this->assertTrue(in_array('config:responsive_image.styles.style_one', $cache_tags)); if (!$empty_styles) { - $this->assertTrue(in_array('config:image.style.thumbnail', $cache_tags)); $this->assertTrue(in_array('config:image.style.medium', $cache_tags)); + $this->assertRaw('type="image/png"'); } $this->assertTrue(in_array('config:image.style.large', $cache_tags)); // Test the fallback image style. - $large_style = entity_load('image_style', 'large'); + $image = \Drupal::service('image.factory')->get($image_uri); $fallback_image = array( - '#theme' => 'responsive_image_source', - '#src' => $large_style->buildUrl($image_uri), - '#dimensions' => array('width' => 40, 'height' => 20), + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => $large_style->buildUrl($image->getSource()), + ), + ), ); $default_output = drupal_render($fallback_image); - $this->assertRaw($default_output, 'Image style thumbnail formatter displaying correctly on full node view.'); + $this->assertRaw($default_output, 'Image style large formatter displaying correctly on full node view.'); if ($scheme == 'private') { // Log out and try to access the file. $this->drupalLogout(); $this->drupalGet($large_style->buildUrl($image_uri)); - $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.'); + $this->assertResponse('403', 'Access denied to image style large as anonymous user.'); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); } } + /** + * Tests responsive image formatters on node display linked to the file. + */ + public function testResponsiveImageFieldFormattersLinkToFile() { + $this->addTestImageStyleMappings(); + $this->assertResponsiveImageFieldFormattersLink('file'); + } + + /** + * Tests responsive image formatters on node display linked to the node. + */ + public function testResponsiveImageFieldFormattersLinkToNode() { + $this->addTestImageStyleMappings(); + $this->assertResponsiveImageFieldFormattersLink('content'); + } + + /** + * Tests responsive image formatters linked to the file or node. + */ + private function assertResponsiveImageFieldFormattersLink($link_type) { + $field_name = Unicode::strtolower($this->randomMachineName()); + $this->createImageField($field_name, 'article', array('uri_scheme' => 'public')); + // Create a new node with an image attached. + $test_image = current($this->drupalGetTestFiles('image')); + $nid = $this->uploadNodeImage($test_image, $field_name, 'article'); + $this->container->get('entity.manager')->getStorage('node')->resetCache(array($nid)); + $node = Node::load($nid); + + // Use the responsive image formatter linked to file formatter. + $display_options = array( + 'type' => 'responsive_image', + 'settings' => array( + 'image_link' => $link_type, + 'responsive_image_style' => 'style_one', + 'fallback_image_style' => 'large', + ), + ); + entity_get_display('node', 'article', 'default') + ->setComponent($field_name, $display_options) + ->save(); + + // Create a derivative so at least one MIME type will be known. + $large_style = ImageStyle::load('large'); + $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); + $large_style->createDerivative($image_uri, $large_style->buildUri($image_uri)); + + // Output should contain all image styles and all breakpoints. + $this->drupalGet('node/' . $nid); + switch ($link_type) { + case 'file': + // Make sure the link to the file is present. + $this->assertPattern('/assertPattern('/url(), '/') . '"(.*?)>drupalPlaceBlock('system_breadcrumb_block'); + // Create a test user. + $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer node fields', 'administer node form display', 'administer node display', 'bypass node access')); + $this->drupalLogin($admin_user); + + // Create content type, with underscores. + $type_name = strtolower($this->randomMachineName(8)) . '_test'; + $type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name)); + $this->type = $type->id(); + } + + /** + * Tests formatter settings. + */ + function testResponsiveImageFormatterUI() { + $manage_fields = 'admin/structure/types/manage/' . $this->type; + $manage_display = $manage_fields . '/display'; + + // Create a field, and a node with some data for the field. + $this->fieldUIAddNewField($manage_fields, 'image', 'Image field', 'image'); + // Display the "Manage display". + $this->drupalGet($manage_display); + + // Change the formatter and check that the summary is updated. + $edit = array('fields[field_image][type]' => 'responsive_image', 'refresh_rows' => 'field_image'); + $this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh'))); + $this->assertText("Select a responsive image style.", 'The expected summary is displayed.'); + + // Submit the form. + $this->drupalPostForm(NULL, array(), t('Save')); + $this->assertText("Select a responsive image style.", 'The expected summary is displayed.'); + + // Create responsive image styles. + $responsive_image_style = entity_create('responsive_image_style', array( + 'id' => 'style_one', + 'label' => 'Style One', + 'breakpoint_group' => 'responsive_image_test_module', + )); + $responsive_image_style + ->addImageStyleMapping('responsive_image_test_module.mobile', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'thumbnail', + )) + ->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'medium' + )) + // Test the normal output of mapping to an image style. + ->addImageStyleMapping('responsive_image_test_module.wide', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )) + ->save(); + \Drupal::entityManager()->clearCachedFieldDefinitions(); + // Refresh the page. + $this->drupalGet($manage_display); + $this->assertText("Select a responsive image style.", 'The expected summary is displayed.'); + + // Click on the formatter settings button to open the formatter settings + // form. + $this->drupalPostAjaxForm(NULL, array(), "field_image_settings_edit"); + + // Assert that the correct fields are present. + $fieldnames = array( + 'fields[field_image][settings_edit_form][settings][responsive_image_style]', + 'fields[field_image][settings_edit_form][settings][fallback_image_style]', + 'fields[field_image][settings_edit_form][settings][image_link]', + ); + foreach ($fieldnames as $fieldname) { + $this->assertField($fieldname); + } + $edit = array( + 'fields[field_image][settings_edit_form][settings][responsive_image_style]' => 'style_one', + 'fields[field_image][settings_edit_form][settings][fallback_image_style]' => 'thumbnail', + 'fields[field_image][settings_edit_form][settings][image_link]' => 'content', + ); + $this->drupalPostAjaxForm(NULL, $edit, "field_image_plugin_settings_update"); + + // Save the form to save the settings. + $this->drupalPostForm(NULL, array(), t('Save')); + $this->assertText('Responsive image style: Style One'); + $this->assertText('Fallback Image style: Thumbnail (100×100)'); + $this->assertText('Linked to content'); + + // Click on the formatter settings button to open the formatter settings + // form. + $this->drupalPostAjaxForm(NULL, array(), "field_image_settings_edit"); + $edit = array( + 'fields[field_image][settings_edit_form][settings][responsive_image_style]' => 'style_one', + 'fields[field_image][settings_edit_form][settings][fallback_image_style]' => '', + 'fields[field_image][settings_edit_form][settings][image_link]' => 'file', + ); + $this->drupalPostAjaxForm(NULL, $edit, "field_image_plugin_settings_update"); + + // Save the form to save the third party settings. + $this->drupalPostForm(NULL, array(), t('Save')); + $this->assertText('Responsive image style: Style One'); + $this->assertText('Automatic fallback'); + $this->assertText('Linked to file'); + } + +} diff --git a/core/modules/responsive_image/templates/responsive-image.html.twig b/core/modules/responsive_image/templates/responsive-image.html.twig new file mode 100644 index 0000000..9a89043 --- /dev/null +++ b/core/modules/responsive_image/templates/responsive-image.html.twig @@ -0,0 +1,30 @@ +{# +/** + * @file + * Default theme implementation of a responsive image. + * + * Available variables: + * - sources: The attributes of the tags for this tag. + * - fallback_image: The fallback tag to use for this tag. + * + * @see template_preprocess() + * @see template_preprocess_responsive_image() + * + * @ingroup themeable + */ +#} + + {% if sources %} + {# + Internet Explorer 9 doesn't recognise source elements that are wrapped in + picture tags. See http://scottjehl.github.io/picturefill/#ie9 + #} + + {% for source_attributes in sources %} + + {% endfor %} + + {% endif %} + {# The controlling image, with the fallback image in srcset. #} + {{ img_element }} + diff --git a/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php b/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php deleted file mode 100644 index df8a0ab..0000000 --- a/core/modules/responsive_image/tests/src/Unit/ResponsiveImageMappingConfigEntityUnitTest.php +++ /dev/null @@ -1,198 +0,0 @@ -entityType = $this->getMock('\Drupal\Core\Entity\EntityTypeInterface'); - $this->entityType->expects($this->any()) - ->method('getProvider') - ->will($this->returnValue('responsive_image')); - - $this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'); - $this->entityManager->expects($this->any()) - ->method('getDefinition') - ->with('responsive_image_mapping') - ->will($this->returnValue($this->entityType)); - - $this->breakpointManager = $this->getMock('\Drupal\breakpoint\BreakpointManagerInterface'); - - $container = new ContainerBuilder(); - $container->set('entity.manager', $this->entityManager); - $container->set('breakpoint.manager', $this->breakpointManager); - \Drupal::setContainer($container); - } - - /** - * @covers ::calculateDependencies - */ - public function testCalculateDependencies() { - $entity = new ResponsiveImageMapping(array('breakpointGroup' => 'test_group')); - $entity->setBreakpointGroup('test_group'); - - $this->breakpointManager->expects($this->any()) - ->method('getGroupProviders') - ->with('test_group') - ->willReturn(array('bartik' => 'theme', 'toolbar' => 'module')); - - $dependencies = $entity->calculateDependencies(); - $this->assertContains('toolbar', $dependencies['module']); - $this->assertContains('bartik', $dependencies['theme']); - } - - /** - * @covers ::addMapping - * @covers ::hasMappings - */ - public function testHasMappings() { - $entity = new ResponsiveImageMapping(array()); - $this->assertFalse($entity->hasMappings()); - $entity->addMapping('test_breakpoint', '1x', 'test_style'); - $this->assertTrue($entity->hasMappings()); - } - - /** - * @covers ::addMapping - * @covers ::getImageStyle - */ - public function testGetImageStyle() { - $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')); - } - - /** - * @covers ::addMapping - * @covers ::getMappings - */ - 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'); - - $expected = array( - 'test_breakpoint' => array( - '1x' => 'test_style', - '2x' => 'test_style2', - ), - 'test_breakpoint2' => array( - '1x' => 'test_style3', - ) - ); - $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'; - $this->assertEquals($expected, $entity->getKeyedMappings()); - } - - /** - * @covers ::addMapping - * @covers ::getMappings - */ - 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'); - - $expected = array( - array( - 'breakpoint_id' => 'test_breakpoint', - 'multiplier' => '1x', - 'image_style' => 'test_style', - ), - array( - 'breakpoint_id' => 'test_breakpoint', - 'multiplier' => '2x', - 'image_style' => 'test_style2', - ), - array( - 'breakpoint_id' => 'test_breakpoint2', - 'multiplier' => '1x', - 'image_style' => 'test_style3', - ), - ); - $this->assertEquals($expected, $entity->getMappings()); - } - - /** - * @covers ::addMapping - * @covers ::removeMappings - */ - 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'); - - $this->assertTrue($entity->hasMappings()); - $entity->removeMappings(); - $this->assertEmpty($entity->getMappings()); - $this->assertEmpty($entity->getKeyedMappings()); - $this->assertFalse($entity->hasMappings()); - } - - /** - * @covers ::setBreakpointGroup - * @covers ::getBreakpointGroup - */ - 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'); - - // Ensure that setting to same group does not remove mappings. - $entity->setBreakpointGroup('test_group'); - $this->assertTrue($entity->hasMappings()); - $this->assertEquals('test_group', $entity->getBreakpointGroup()); - - // Ensure that changing the group removes mappings. - $entity->setBreakpointGroup('test_group2'); - $this->assertEquals('test_group2', $entity->getBreakpointGroup()); - $this->assertFalse($entity->hasMappings()); - } - -} diff --git a/core/modules/responsive_image/tests/src/Unit/ResponsiveImageStyleConfigEntityUnitTest.php b/core/modules/responsive_image/tests/src/Unit/ResponsiveImageStyleConfigEntityUnitTest.php new file mode 100644 index 0000000..afbb26f --- /dev/null +++ b/core/modules/responsive_image/tests/src/Unit/ResponsiveImageStyleConfigEntityUnitTest.php @@ -0,0 +1,339 @@ +entityType = $this->getMock('\Drupal\Core\Entity\EntityTypeInterface'); + $this->entityType->expects($this->any()) + ->method('getProvider') + ->will($this->returnValue('responsive_image')); + + $this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'); + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->with('responsive_image_style') + ->will($this->returnValue($this->entityType)); + + $this->breakpointManager = $this->getMock('\Drupal\breakpoint\BreakpointManagerInterface'); + + $container = new ContainerBuilder(); + $container->set('entity.manager', $this->entityManager); + $container->set('breakpoint.manager', $this->breakpointManager); + \Drupal::setContainer($container); + } + + /** + * @covers ::calculateDependencies + */ + public function testCalculateDependencies() { + $entity = new ResponsiveImageStyle(array('breakpoint_group' => 'test_group')); + $entity->setBreakpointGroup('test_group'); + + $this->breakpointManager->expects($this->any()) + ->method('getGroupProviders') + ->with('test_group') + ->willReturn(array('bartik' => 'theme', 'toolbar' => 'module')); + + $dependencies = $entity->calculateDependencies(); + $this->assertContains('toolbar', $dependencies['module']); + $this->assertContains('bartik', $dependencies['theme']); + } + + /** + * @covers ::addImageStyleMapping + * @covers ::hasImageStyleMappings + */ + public function testHasImageStyleMappings() { + $entity = new ResponsiveImageStyle(array()); + $this->assertFalse($entity->hasImageStyleMappings()); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => '', + )); + $this->assertFalse($entity->hasImageStyleMappings()); + $entity->removeImageStyleMappings(); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array(), + ), + )); + $this->assertFalse($entity->hasImageStyleMappings()); + $entity->removeImageStyleMappings(); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + )); + $this->assertFalse($entity->hasImageStyleMappings()); + $entity->removeImageStyleMappings(); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )); + $this->assertTrue($entity->hasImageStyleMappings()); + $entity->removeImageStyleMappings(); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + )); + $this->assertTrue($entity->hasImageStyleMappings()); + } + + /** + * @covers ::addImageStyleMapping + * @covers ::getImageStyleMapping + */ + public function testGetImageStyleMapping() { + $entity = new ResponsiveImageStyle(array('')); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )); + $expected = array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + ); + $this->assertEquals($expected, $entity->getImageStyleMapping('test_breakpoint', '1x')); + $this->assertNull($entity->getImageStyleMapping('test_unknown_breakpoint', '1x')); + } + + /** + * @covers ::addImageStyleMapping + * @covers ::getKeyedImageStyleMappings + */ + public function testGetKeyedImageStyleMappings() { + $entity = new ResponsiveImageStyle(array('')); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )); + $entity->addImageStyleMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + )); + $entity->addImageStyleMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'thumbnail', + )); + + $expected = array( + 'test_breakpoint' => array( + '1x' => array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + ), + '2x' => array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '2x', + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + ), + ), + 'test_breakpoint2' => array( + '1x' => array( + 'breakpoint_id' => 'test_breakpoint2', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'thumbnail', + ), + ) + ); + $this->assertEquals($expected, $entity->getKeyedImageStyleMappings()); + + // Add another mapping to ensure keyed mapping static cache is rebuilt. + $entity->addImageStyleMapping('test_breakpoint2', '2x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'medium', + )); + $expected['test_breakpoint2']['2x'] = array( + 'breakpoint_id' => 'test_breakpoint2', + 'multiplier' => '2x', + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'medium', + ); + $this->assertEquals($expected, $entity->getKeyedImageStyleMappings()); + } + + /** + * @covers ::addImageStyleMapping + * @covers ::getImageStyleMappings + */ + public function testGetImageStyleMappings() { + $entity = new ResponsiveImageStyle(array('')); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )); + $entity->addImageStyleMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + )); + $entity->addImageStyleMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'thumbnail', + )); + + $expected = array( + array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + ), + array( + 'breakpoint_id' => 'test_breakpoint', + 'multiplier' => '2x', + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + ), + array( + 'breakpoint_id' => 'test_breakpoint2', + 'multiplier' => '1x', + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'thumbnail', + ), + ); + $this->assertEquals($expected, $entity->getImageStyleMappings()); + } + + /** + * @covers ::addImageStyleMapping + * @covers ::removeImageStyleMappings + */ + public function testRemoveImageStyleMappings() { + $entity = new ResponsiveImageStyle(array('')); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )); + $entity->addImageStyleMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + )); + $entity->addImageStyleMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'thumbnail', + )); + + $this->assertTrue($entity->hasImageStyleMappings()); + $entity->removeImageStyleMappings(); + $this->assertEmpty($entity->getImageStyleMappings()); + $this->assertEmpty($entity->getKeyedImageStyleMappings()); + $this->assertFalse($entity->hasImageStyleMappings()); + } + + /** + * @covers ::setBreakpointGroup + * @covers ::getBreakpointGroup + */ + public function testSetBreakpointGroup() { + $entity = new ResponsiveImageStyle(array('breakpoint_group' => 'test_group')); + $entity->addImageStyleMapping('test_breakpoint', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'large', + )); + $entity->addImageStyleMapping('test_breakpoint', '2x', array( + 'image_mapping_type' => 'sizes', + 'image_mapping' => array( + 'sizes' => '(min-width:700px) 700px, 100vw', + 'sizes_image_styles' => array( + 'large' => 'large', + ), + ), + )); + $entity->addImageStyleMapping('test_breakpoint2', '1x', array( + 'image_mapping_type' => 'image_style', + 'image_mapping' => 'thumbnail', + )); + + // Ensure that setting to same group does not remove mappings. + $entity->setBreakpointGroup('test_group'); + $this->assertTrue($entity->hasImageStyleMappings()); + $this->assertEquals('test_group', $entity->getBreakpointGroup()); + + // Ensure that changing the group removes mappings. + $entity->setBreakpointGroup('test_group2'); + $this->assertEquals('test_group2', $entity->getBreakpointGroup()); + $this->assertFalse($entity->hasImageStyleMappings()); + } + +} 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