first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
---
label: 'Configuring a responsive image style'
related:
- core.media
- field_ui.manage_display
- layout_builder.overview
- image.style
- breakpoint.overview
---
{% set media_topic = render_var(help_topic_link('core.media')) %}
{% set image_style_topic = render_var(help_topic_link('image.style')) %}
{% set breakpoint_overview_topic = render_var(help_topic_link('breakpoint.overview')) %}
{% set styles_link_text %}{% trans %}Responsive image styles{% endtrans %}{% endset %}
{% set styles_link = render_var(help_route_link(styles_link_text, 'entity.responsive_image_style.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure a responsive image style, which can be used to display images at different sizes on different devices. See {{ media_topic }} for an overview of responsive image styles, and {{ breakpoint_overview_topic }} for an overview of breakpoints.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Media</em> &gt; <em>{{ styles_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add responsive image style</em>.{% endtrans %}</li>
<li>{% trans %}Enter a descriptive <em>Label</em> for your style.{% endtrans %}</li>
<li>{% trans %}Select a <em>Breakpoint group</em> from the groups defined by your installed themes and modules.{% endtrans %}</li>
<li>{% trans %}Select a <em>Fallback image style</em> to use when none of the other styles apply. See {{ image_style_topic }} if you need to add a new style.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}On the next page, locate the fieldsets for the breakpoints provided by the selected <em>Breakpoint group</em>.{% endtrans %}</li>
<li>{% trans %}For each breakpoint that you want to use, expand the corresponding fieldset. Select the <em>Select a single image style.</em> radio button under <em>Type</em> for the breakpoint, and select the <em>Image style</em> to use for images when that breakpoint is in effect. Repeat this step for the rest of the breakpoints you want to use.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>{% endtrans %}</li>
<li>{% trans %}You can now use this responsive image style to format a field containing an image, in your layouts or traditional field displays. See related topics below for more information.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,21 @@
id: d7_responsive_image_styles
label: Responsive Image Styles
migration_tags:
- Drupal 7
- Configuration
source:
plugin: d7_responsive_image_styles
process:
id: machine_name
label: label
image_style_mappings:
plugin: image_style_mappings
source:
- mapping
- breakpoint_group
breakpoint_group: breakpoint_group
destination:
plugin: entity:responsive_image_style
migration_dependencies:
required:
- d7_image_styles

View File

@@ -0,0 +1,3 @@
finished:
7:
picture: responsive_image

View File

@@ -0,0 +1,6 @@
responsive_image.viewport_sizing:
label: Viewport Sizing
mediaQuery: ''
weight: 0
multipliers:
- 1x

View File

@@ -0,0 +1,14 @@
name: Responsive Image
type: module
description: 'Provides functionality to output responsive images using the HTML5 picture tag.'
package: Core
# version: VERSION
dependencies:
- drupal:breakpoint
- drupal:image
configure: entity.responsive_image_style.collection
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,5 @@
responsive_image.style_page_add:
route_name: responsive_image.style_page_add
title: 'Add responsive image style'
appears_on:
- entity.responsive_image_style.collection

View File

@@ -0,0 +1,6 @@
entity.responsive_image_style.collection:
title: 'Responsive image styles'
description: 'Manage responsive image styles.'
weight: 10
route_name: entity.responsive_image_style.collection
parent: system.admin_config_media

View File

@@ -0,0 +1,5 @@
entity.responsive_image_style.edit_form:
title: Edit
route_name: entity.responsive_image_style.edit_form
base_route: entity.responsive_image_style.edit_form
weight: -10

View File

@@ -0,0 +1,553 @@
<?php
/**
* @file
* Responsive image display formatter for image fields.
*/
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Url;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\image\Entity\ImageStyle;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\responsive_image\ResponsiveImageConfigUpdater;
use Drupal\responsive_image\ResponsiveImageStyleInterface;
use Drupal\breakpoint\BreakpointInterface;
/**
* Implements hook_help().
*/
function responsive_image_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.responsive_image':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Responsive Image module provides an image formatter that allows browsers to select which image file to display based on media queries or which image file types the browser supports, using the HTML 5 picture and source elements and/or the sizes, srcset and type attributes. For more information, see the <a href=":responsive_image">online documentation for the Responsive Image module</a>.', [':responsive_image' => 'https://www.drupal.org/documentation/modules/responsive_image']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Defining responsive image styles') . '</dt>';
$output .= '<dd>' . t('By creating responsive image styles you define which options the browser has in selecting which image file to display. In most cases this means providing different image sizes based on the viewport size. On the <a href=":responsive_image_style">Responsive image styles</a> page, click <em>Add responsive image style</em> to create a new style. First choose a label, a fallback image style and a breakpoint group and click Save.', [':responsive_image_style' => Url::fromRoute('entity.responsive_image_style.collection')->toString()]) . '</dd>';
$output .= '<dl>';
$output .= '<dt>' . t('Fallback image style') . '</dt>';
$output .= '<dd>' . t('The fallback image style is typically the smallest size image you expect to appear in this space. The fallback image should only appear on a site if an error occurs.') . '</dd>';
$output .= '<dt>' . t('Breakpoint groups: viewport sizing vs art direction') . '</dt>';
$output .= '<dd>' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do <em>viewport sizing.</em> Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as <em>art direction.</em> A new breakpoint group should be created for each aspect ratio to avoid content shift. Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option <em>do not use this breakpoint</em> is selected for each breakpoint. See the <a href=":breakpoint_help">help page of the Breakpoint module</a> for more information.', [':breakpoint_help' => Url::fromRoute('help.page', ['name' => 'breakpoint'])->toString()]) . '</dd>';
$output .= '<dt>' . t('Breakpoint settings: sizes vs image styles') . '</dt>';
$output .= '<dd>' . t('While you have the option to provide only one image style per breakpoint, the sizes attribute allows you to provide more options to browsers as to which image file it can display. If using sizes field and art direction, all selected image styles should use the same aspect ratio to avoid content shifting. Breakpoints are defined in the configuration files of the theme.') . '</dd>';
$output .= '<dt>' . t('Sizes field') . '</dt>';
$output .= '<dd>' . t('The sizes attribute paired with the srcset attribute provides information on how much space these images take up within the viewport at different browser breakpoints, but the aspect ratios should remain the same across those breakpoints. Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the <em>Sizes</em> field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. <em>Media conditions</em> are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the <em>image width</em> at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '</dd>';
$output .= '<dt>' . t('Image styles for sizes') . '</dt>';
$output .= '<dd>' . t('Below the Sizes field you can choose multiple image styles so the browser can choose the best image file size to fill the space defined in the Sizes field. Typically you will want to use image styles that resize your image to have options that range from the smallest px width possible for the space the image will appear in to the largest px width possible, with a variety of widths in between. You may want to provide image styles with widths that are 1.5x to 2x the space available in the layout to account for high resolution screens. Image styles can be defined on the <a href=":image_styles">Image styles page</a> that is provided by the <a href=":image_help">Image module</a>.', [':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(), ':image_help' => Url::fromRoute('help.page', ['name' => 'image'])->toString()]) . '</dd>';
$output .= '</dl></dd>';
$output .= '<dt>' . t('Using responsive image styles in Image fields') . '</dt>';
$output .= '<dd>' . 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 <em>Responsive image</em>, 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 <a href=":field_ui">Field UI module help page</a>. For background information about entities and fields see the <a href=":field_help">Field module help page</a>.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.responsive_image_style.collection':
return '<p>' . t('A responsive image style associates an image style with each breakpoint defined by your theme.') . '</p>';
}
}
/**
* Implements hook_theme().
*/
function responsive_image_theme() {
return [
'responsive_image' => [
'variables' => [
'uri' => NULL,
'attributes' => [],
'responsive_image_style_id' => [],
'height' => NULL,
'width' => NULL,
],
],
'responsive_image_formatter' => [
'variables' => [
'item' => NULL,
'item_attributes' => NULL,
'url' => NULL,
'responsive_image_style_id' => NULL,
],
],
];
}
/**
* Prepares variables for responsive image formatter templates.
*
* Default template: responsive-image-formatter.html.twig.
*
* @param array $variables
* An associative array containing:
* - item: An ImageItem object.
* - item_attributes: An optional associative array of HTML attributes to be
* placed in the img tag.
* - responsive_image_style_id: A responsive image style.
* - url: An optional \Drupal\Core\Url object.
*/
function template_preprocess_responsive_image_formatter(&$variables) {
// Provide fallback to standard image if valid responsive image style is not
// provided in the responsive image formatter.
$responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']);
if ($responsive_image_style) {
$variables['responsive_image'] = [
'#type' => 'responsive_image',
'#responsive_image_style_id' => $variables['responsive_image_style_id'],
];
}
else {
$variables['responsive_image'] = [
'#theme' => 'image',
];
}
$item = $variables['item'];
$attributes = [];
// Do not output an empty 'title' attribute.
if (!is_null($item->title) && mb_strlen($item->title) != 0) {
$attributes['title'] = $item->title;
}
$attributes['alt'] = $item->alt;
// Need to check that item_attributes has a value since it can be NULL.
if ($variables['item_attributes']) {
$attributes += $variables['item_attributes'];
}
if (($entity = $item->entity) && empty($item->uri)) {
$variables['responsive_image']['#uri'] = $entity->getFileUri();
}
else {
$variables['responsive_image']['#uri'] = $item->uri;
}
foreach (['width', 'height'] as $key) {
$variables['responsive_image']["#$key"] = $item->$key;
}
$variables['responsive_image']['#attributes'] = $attributes;
}
/**
* Prepares variables for a responsive image.
*
* Default template: responsive-image.html.twig.
*
* @param $variables
* An associative array containing:
* - uri: The URI of the image.
* - width: The width of the image (if known).
* - height: The height of the image (if known).
* - attributes: Associative array of attributes to be placed in the img tag.
* - responsive_image_style_id: The ID of the responsive image style.
*/
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/
if (isset($variables['width']) && empty($variables['width'])) {
unset($variables['width']);
unset($variables['height']);
}
elseif (isset($variables['height']) && empty($variables['height'])) {
unset($variables['width']);
unset($variables['height']);
}
$responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']);
// If a responsive image style is not selected, log the error and stop
// execution.
if (!$responsive_image_style) {
$variables['img_element'] = [];
\Drupal::logger('responsive_image')->log(RfcLogLevel::ERROR, 'Failed to load responsive image style: “@style“ while displaying responsive image.', ['@style' => $variables['responsive_image_style_id']]);
return;
}
// Retrieve all breakpoints and multipliers and reverse order of breakpoints.
// By default, breakpoints are ordered from smallest weight to largest:
// the smallest weight is expected to have the smallest breakpoint width,
// while the largest weight is expected to have the largest breakpoint
// width. For responsive images, we need largest breakpoint widths first, so
// we need to reverse the order of these breakpoints.
$breakpoints = array_reverse(\Drupal::service('breakpoint.manager')->getBreakpointsByGroup($responsive_image_style->getBreakpointGroup()));
foreach ($responsive_image_style->getKeyedImageStyleMappings() as $breakpoint_id => $multipliers) {
if (isset($breakpoints[$breakpoint_id])) {
$variables['sources'][] = _responsive_image_build_source_attributes($variables, $breakpoints[$breakpoint_id], $multipliers);
}
}
if (isset($variables['sources']) && count($variables['sources']) === 1 && !isset($variables['sources'][0]['media'])) {
// There is only one source tag with an empty media attribute. This means
// we can output an image tag with the srcset attribute instead of a
// picture tag.
$variables['output_image_tag'] = TRUE;
foreach ($variables['sources'][0] as $attribute => $value) {
if ($attribute != 'type') {
$variables['attributes'][$attribute] = $value;
}
}
$variables['img_element'] = [
'#theme' => 'image',
'#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
'#attributes' => [],
];
}
else {
$variables['output_image_tag'] = FALSE;
// Prepare the fallback image. We use the src attribute, which might cause
// double downloads in browsers that don't support the picture tag.
$variables['img_element'] = [
'#theme' => 'image',
'#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
'#attributes' => [],
];
}
// Get width and height from fallback responsive image style and transfer them
// to img tag so browser can do aspect ratio calculation and prevent
// recalculation of layout on image load.
$dimensions = responsive_image_get_image_dimensions($responsive_image_style->getFallbackImageStyle(), [
'width' => $variables['width'],
'height' => $variables['height'],
],
$variables['uri']
);
$variables['img_element']['#width'] = $dimensions['width'];
$variables['img_element']['#height'] = $dimensions['height'];
if (isset($variables['attributes'])) {
if (isset($variables['attributes']['alt'])) {
$variables['img_element']['#alt'] = $variables['attributes']['alt'];
unset($variables['attributes']['alt']);
}
if (isset($variables['attributes']['title'])) {
$variables['img_element']['#title'] = $variables['attributes']['title'];
unset($variables['attributes']['title']);
}
$variables['img_element']['#attributes'] = $variables['attributes'];
}
}
/**
* Helper function for template_preprocess_responsive_image().
*
* Builds an array of attributes for <source> tags to be used in a <picture>
* tag. In other words, this function provides the attributes for each <source>
* tag in a <picture> 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 <source> tag with the 'sizes' attribute) or
* 'image_style' (meaning it will output a <source> 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([
* 'id' => 'style_one',
* 'label' => 'Style One',
* 'breakpoint_group' => 'responsive_image_test_module',
* ]);
* $responsive_img_style->addImageStyleMapping('responsive_image_test_module.mobile', '1x', [
* 'image_mapping_type' => 'image_style',
* 'image_mapping' => 'thumbnail',
* ])
* ->addImageStyleMapping('responsive_image_test_module.narrow', '1x', [
* 'image_mapping_type' => 'sizes',
* 'image_mapping' => [
* 'sizes' => '(min-width: 700px) 700px, 100vw',
* 'sizes_image_styles' => [
* 'large' => 'large',
* 'medium' => 'medium',
* ],
* ],
* ])
* ->save();
* @endcode
* The above responsive image style will result in a <picture> tag like this:
* @code
* <picture>
* <source media="(min-width: 0px)" srcset="sites/default/files/styles/thumbnail/image.jpeg" />
* <source media="(min-width: 560px)" sizes="(min-width: 700px) 700px, 100vw" srcset="sites/default/files/styles/large/image.jpeg 480w, sites/default/files/styles/medium/image.jpeg 220w" />
* <img src="fallback.jpeg" />
* </picture>
* @endcode
*
* When all the images in the 'srcset' attribute of a <source> 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 (<source> tag) to load based on the MIME types they support (which, for
* instance, can be beneficial for browsers supporting WebP).
* For example:
* A <source> tag can contain multiple images:
* @code
* <source [...] srcset="image1.jpeg 1x, image2.jpeg 2x, image3.jpeg 3x" />
* @endcode
* In the above example we can add the 'mime-type' attribute ('image/jpeg')
* since all images in the 'srcset' attribute of the <source> tag have the same
* MIME type.
* If a <source> tag were to look like this:
* @code
* <source [...] srcset="image1.jpeg 1x, image2.webp 2x, image3.jpeg 3x" />
* @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 <source> tag all images in the
* 'srcset' attribute of the <source> tag need to be of the same MIME type. This
* way, a <picture> tag could look like this:
* @code
* <picture>
* <source [...] mime-type="image/webp" srcset="image1.webp 1x, image2.webp 2x, image3.webp 3x"/>
* <source [...] mime-type="image/jpeg" srcset="image1.jpeg 1x, image2.jpeg 2x, image3.jpeg 3x"/>
* <img src="fallback.jpeg" />
* </picture>
* @endcode
* This way a browser can decide which <source> tag is preferred based on the
* MIME type. In other words, the MIME types of all images in one <source> tag
* need to be the same in order to set the 'mime-type' attribute but not all
* MIME types within the <picture> 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
* <source media="(min-width: 0px)" srcset="image1.jpeg 100w" />
* @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
* <source media="(min-width:0px)" srcset="img1.jpeg 50w, img2.jpeg=100w" />
* @endcode
* Invalid:
* @code
* <source media="(min-width:0px)" srcset="img1.jpeg 50w 1x, img2.jpeg=100w 1x" />
* @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
* <source [...] srcset="image1.jpeg 1x, image2.webp 2x, image3.jpeg 3x" />
* @endcode
* to support multipliers or
* @code
* <source [...] sizes"(min-width: 40em) 80vw, 100vw" srcset="image1.jpeg 300w, image2.webp 600w, image3.jpeg 1200w" />
* @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
* <source [...] sizes="(min-width: 40em) 80vw, 100vw" srcset="a1.jpeg 300w 1x, a2.jpeg 600w 1x, a3.jpeg 1200w 1x, b1.jpeg 250w 2x, b2.jpeg 680w 2x, b3.jpeg 1240w 2x" />
* @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
* <source [...] sizes="(min-width: 40em) 80vw, 100vw" srcset="b1.jpeg 250w, a1.jpeg 300w, a2.jpeg 600w, b2.jpeg 680w, a3.jpeg 1200w, b3.jpeg 1240w" />
* @endcode
* See http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#image-candidate-string
* for further information.
*
* @param array $variables
* 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).
* - uri: The URI of the image file.
* @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.
*
* @return \Drupal\Core\Template\Attribute
* An object of attributes for the source tag.
*/
function _responsive_image_build_source_attributes(array $variables, BreakpointInterface $breakpoint, array $multipliers) {
if ((empty($variables['width']) || empty($variables['height']))) {
$image = \Drupal::service('image.factory')->get($variables['uri']);
$width = $image->getWidth();
$height = $image->getHeight();
}
else {
$width = $variables['width'];
$height = $variables['height'];
}
$extension = pathinfo($variables['uri'], PATHINFO_EXTENSION);
$sizes = [];
$srcset = [];
$derivative_mime_types = [];
// Traverse the multipliers in reverse so the largest image is processed last.
// The last image's dimensions are used for img.srcset height and width.
foreach (array_reverse($multipliers) as $multiplier => $image_style_mapping) {
switch ($image_style_mapping['image_mapping_type']) {
// Create a <source> 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, ['width' => $width, 'height' => $height], $variables['uri']);
// 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("Could not determine image width for '{$variables['uri']}' 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.");
}
// Use the image width as key so we can sort the array later on.
// Images within a srcset should be sorted from small to large, since
// the first matching source will be used.
$srcset[intval($dimensions['width'])] = _responsive_image_image_style_url($image_style_name, $variables['uri']) . ' ' . $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. Use the multiplier as key
// so we can sort the array later on. Multipliers within a srcset should
// be sorted from small to large, since the first matching source will
// be used. We multiply it by 100 so multipliers with up to two decimals
// can be used.
$srcset[intval(mb_substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $variables['uri']) . ' ' . $multiplier;
$dimensions = responsive_image_get_image_dimensions($image_style_mapping['image_mapping'], ['width' => $width, 'height' => $height], $variables['uri']);
break;
}
}
// Sort the srcset from small to large image width or multiplier.
ksort($srcset);
$source_attributes = new Attribute([
'srcset' => implode(', ', array_unique($srcset)),
]);
$media_query = trim($breakpoint->getMediaQuery());
if (!empty($media_query)) {
$source_attributes->setAttribute('media', $media_query);
}
if (count(array_unique($derivative_mime_types)) == 1) {
$source_attributes->setAttribute('type', $derivative_mime_types[0]);
}
if (!empty($sizes)) {
$source_attributes->setAttribute('sizes', implode(',', array_unique($sizes)));
}
// The images used in a particular srcset attribute should all have the same
// aspect ratio. The sizes attribute paired with the srcset attribute provides
// information on how much space these images take up within the viewport at
// different breakpoints, but the aspect ratios should remain the same across
// those breakpoints. Multiple source elements can be used for art direction,
// where aspect ratios should change at particular breakpoints. Each source
// element can still have srcset and sizes attributes to handle variations for
// that particular aspect ratio. Because the same aspect ratio is assumed for
// all images in a srcset, dimensions are always added to the source
// attribute. Within srcset, images are sorted from largest to smallest in
// terms of the real dimension of the image.
if (!empty($dimensions['width']) && !empty($dimensions['height'])) {
$source_attributes->setAttribute('width', $dimensions['width']);
$source_attributes->setAttribute('height', $dimensions['height']);
}
return $source_attributes;
}
/**
* Determines the dimensions of an image.
*
* @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:
* - width: The width of the source image (if known).
* - height: The height of the source image (if known).
* @param string $uri
* The URI of the image file.
*
* @return array
* Dimensions to be modified - an array with components width and height, in
* pixels.
*/
function responsive_image_get_image_dimensions($image_style_name, array $dimensions, $uri) {
// Determine the dimensions of the styled image.
if ($image_style_name == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
$dimensions = [
'width' => 1,
'height' => 1,
];
}
elseif ($entity = ImageStyle::load($image_style_name)) {
$entity->transformDimensions($dimensions, $uri);
}
return $dimensions;
}
/**
* 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 == ResponsiveImageStyleInterface::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.
if ($image_style_name === ResponsiveImageStyleInterface::ORIGINAL_IMAGE) {
$fake_path = 'responsive_image.' . $extension;
}
else {
$fake_path = 'responsive_image.' . ImageStyle::load($image_style_name)->getDerivativeExtension($extension);
}
return \Drupal::service('file.mime_type.guesser.extension')->guessMimeType($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 == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
// The smallest data URI for a 1px square transparent GIF image.
// http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
}
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
$entity = ImageStyle::load($style_name);
if ($entity instanceof ImageStyle) {
return $file_url_generator->transformRelative($entity->buildUrl($path));
}
return $file_url_generator->generateString($path);
}
/**
* Implements hook_library_info_alter().
*
* Load responsive_image.js whenever ajax is added.
*/
function responsive_image_library_info_alter(array &$libraries, $module) {
if ($module === 'core' && isset($libraries['drupal.ajax'])) {
$libraries['drupal.ajax']['dependencies'][] = 'responsive_image/ajax';
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for entity_view_display.
*/
function responsive_image_entity_view_display_presave(EntityViewDisplayInterface $view_display): void {
/** @var \Drupal\responsive_image\ResponsiveImageConfigUpdater $config_updater */
$config_updater = \Drupal::classResolver(ResponsiveImageConfigUpdater::class);
$config_updater->processResponsiveImageField($view_display);
}

View File

@@ -0,0 +1,2 @@
administer responsive images:
title: 'Administer responsive images'

View File

@@ -0,0 +1,43 @@
<?php
/**
* @file
* Post update functions for Responsive Image.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\responsive_image\ResponsiveImageConfigUpdater;
use Drupal\responsive_image\ResponsiveImageStyleInterface;
/**
* Implements hook_removed_post_updates().
*/
function responsive_image_removed_post_updates() {
return [
'responsive_image_post_update_recreate_dependencies' => '9.0.0',
];
}
/**
* Re-order mappings by breakpoint ID and descending numeric multiplier order.
*/
function responsive_image_post_update_order_multiplier_numerically(?array &$sandbox = NULL): void {
/** @var \Drupal\responsive_image\ResponsiveImageConfigUpdater $responsive_image_config_updater */
$responsive_image_config_updater = \Drupal::classResolver(ResponsiveImageConfigUpdater::class);
$responsive_image_config_updater->setDeprecationsEnabled(FALSE);
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'responsive_image_style', function (ResponsiveImageStyleInterface $responsive_image_style) use ($responsive_image_config_updater): bool {
return $responsive_image_config_updater->orderMultipliersNumerically($responsive_image_style);
});
}
/**
* Add the image loading settings to responsive image field formatter instances.
*/
function responsive_image_post_update_image_loading_attribute(?array &$sandbox = NULL): void {
$responsive_image_config_updater = \Drupal::classResolver(ResponsiveImageConfigUpdater::class);
$responsive_image_config_updater->setDeprecationsEnabled(FALSE);
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', function (EntityViewDisplayInterface $view_display) use ($responsive_image_config_updater): bool {
return $responsive_image_config_updater->processResponsiveImageField($view_display);
});
}

View File

@@ -0,0 +1,39 @@
entity.responsive_image_style.collection:
path: '/admin/config/media/responsive-image-style'
defaults:
_entity_list: 'responsive_image_style'
_title: 'Responsive image styles'
requirements:
_permission: 'administer responsive images'
responsive_image.style_page_add:
path: '/admin/config/media/responsive-image-style/add'
defaults:
_entity_form: 'responsive_image_style.add'
_title: 'Add responsive image style'
requirements:
_permission: 'administer responsive images'
entity.responsive_image_style.edit_form:
path: '/admin/config/media/responsive-image-style/{responsive_image_style}'
defaults:
_entity_form: 'responsive_image_style.edit'
_title: 'Edit responsive image style'
requirements:
_permission: 'administer responsive images'
entity.responsive_image_style.duplicate_form:
path: '/admin/config/media/responsive-image-style/{responsive_image_style}/duplicate'
defaults:
_entity_form: 'responsive_image_style.duplicate'
_title: 'Duplicate responsive image style'
requirements:
_permission: 'administer responsive images'
entity.responsive_image_style.delete_form:
path: '/admin/config/media/responsive-image-style/{responsive_image_style}/delete'
defaults:
_entity_form: 'responsive_image_style.delete'
_title: 'Delete'
requirements:
_permission: 'administer responsive images'

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\responsive_image\Element;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element\RenderElementBase;
/**
* Provides a responsive image element.
*/
#[RenderElement('responsive_image')]
class ResponsiveImage extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#theme' => 'responsive_image',
];
}
}

View File

@@ -0,0 +1,313 @@
<?php
namespace Drupal\responsive_image\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\image\Entity\ImageStyle;
use Drupal\responsive_image\ResponsiveImageConfigUpdater;
use Drupal\responsive_image\ResponsiveImageStyleInterface;
/**
* Defines the responsive image style entity.
*
* @ConfigEntityType(
* id = "responsive_image_style",
* label = @Translation("Responsive image style"),
* label_collection = @Translation("Responsive image styles"),
* label_singular = @Translation("responsive image style"),
* label_plural = @Translation("responsive image styles"),
* label_count = @PluralTranslation(
* singular = "@count responsive image style",
* plural = "@count responsive image styles",
* ),
* handlers = {
* "list_builder" = "Drupal\responsive_image\ResponsiveImageStyleListBuilder",
* "form" = {
* "edit" = "Drupal\responsive_image\ResponsiveImageStyleForm",
* "add" = "Drupal\responsive_image\ResponsiveImageStyleForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* "duplicate" = "Drupal\responsive_image\ResponsiveImageStyleForm"
* }
* },
* admin_permission = "administer responsive images",
* config_prefix = "styles",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "image_style_mappings",
* "breakpoint_group",
* "fallback_image_style",
* },
* links = {
* "edit-form" = "/admin/config/media/responsive-image-style/{responsive_image_style}",
* "duplicate-form" = "/admin/config/media/responsive-image-style/{responsive_image_style}/duplicate",
* "delete-form" = "/admin/config/media/responsive-image-style/{responsive_image_style}/delete",
* "collection" = "/admin/config/media/responsive-image-style",
* }
* )
*/
class ResponsiveImageStyle extends ConfigEntityBase implements ResponsiveImageStyleInterface {
/**
* The responsive image ID (machine name).
*
* @var string
*/
protected $id;
/**
* The responsive image label.
*
* @var string
*/
protected $label;
/**
* The image style mappings.
*
* Each image style mapping array contains the following keys:
* - image_mapping_type: Either 'image_style' or 'sizes'.
* - image_mapping:
* - If image_mapping_type is 'image_style', the image style ID (a
* string).
* - If image_mapping_type is 'sizes', an array with following keys:
* - sizes: The value for the 'sizes' attribute.
* - sizes_image_styles: The image styles to use for the 'srcset'
* attribute.
* - breakpoint_id: The breakpoint ID for this image style mapping.
* - multiplier: The multiplier for this image style mapping.
*
* @var array
*/
protected $image_style_mappings = [];
/**
* @var array
*/
protected $keyedImageStyleMappings;
/**
* The responsive image breakpoint group.
*
* @var string
*/
protected $breakpoint_group = '';
/**
* The fallback image style.
*
* @var string
*/
protected $fallback_image_style = '';
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type_id = 'responsive_image_style') {
parent::__construct($values, $entity_type_id);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
$config_updater = \Drupal::classResolver(ResponsiveImageConfigUpdater::class);
$config_updater->orderMultipliersNumerically($this);
}
/**
* {@inheritdoc}
*/
public function addImageStyleMapping($breakpoint_id, $multiplier, array $image_style_mapping) {
// If there is an existing mapping, overwrite it.
foreach ($this->image_style_mappings as &$mapping) {
if ($mapping['breakpoint_id'] === $breakpoint_id && $mapping['multiplier'] === $multiplier) {
$mapping = $image_style_mapping + [
'breakpoint_id' => $breakpoint_id,
'multiplier' => $multiplier,
];
$this->sortMappings();
return $this;
}
}
$this->image_style_mappings[] = $image_style_mapping + [
'breakpoint_id' => $breakpoint_id,
'multiplier' => $multiplier,
];
$this->sortMappings();
return $this;
}
/**
* Sort mappings by breakpoint ID and multiplier.
*/
protected function sortMappings(): void {
$this->keyedImageStyleMappings = NULL;
$breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup($this->getBreakpointGroup());
if (empty($breakpoints)) {
return;
}
usort($this->image_style_mappings, static function (array $a, array $b) use ($breakpoints): int {
$breakpoint_a = $breakpoints[$a['breakpoint_id']] ?? NULL;
$breakpoint_b = $breakpoints[$b['breakpoint_id']] ?? NULL;
$first = ((float) mb_substr($a['multiplier'], 0, -1)) * 100;
$second = ((float) mb_substr($b['multiplier'], 0, -1)) * 100;
return [$breakpoint_b ? $breakpoint_b->getWeight() : 0, $first] <=> [$breakpoint_a ? $breakpoint_a->getWeight() : 0, $second];
});
}
/**
* {@inheritdoc}
*/
public function hasImageStyleMappings() {
$mappings = $this->getKeyedImageStyleMappings();
return !empty($mappings);
}
/**
* {@inheritdoc}
*/
public function getKeyedImageStyleMappings() {
if (!$this->keyedImageStyleMappings) {
$this->keyedImageStyleMappings = [];
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 setFallbackImageStyle($fallback_image_style) {
$this->fallback_image_style = $fallback_image_style;
return $this;
}
/**
* {@inheritdoc}
*/
public function getFallbackImageStyle() {
return $this->fallback_image_style;
}
/**
* {@inheritdoc}
*/
public function removeImageStyleMappings() {
$this->image_style_mappings = [];
$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);
}
// Extract all the styles from the image style mappings.
$styles = ImageStyle::loadMultiple($this->getImageStyleIds());
array_walk($styles, function ($style) {
$this->addDependency('config', $style->getConfigDependencyName());
});
return $this;
}
/**
* {@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];
}
}
/**
* {@inheritdoc}
*/
public function getImageStyleIds() {
$image_styles = [$this->getFallbackImageStyle()];
foreach ($this->getImageStyleMappings() as $image_style_mapping) {
// Only image styles of non-empty mappings should be loaded.
if (!$this::isEmptyImageStyleMapping($image_style_mapping)) {
switch ($image_style_mapping['image_mapping_type']) {
case 'image_style':
$image_styles[] = $image_style_mapping['image_mapping'];
break;
case 'sizes':
$image_styles = array_merge($image_styles, $image_style_mapping['image_mapping']['sizes_image_styles']);
break;
}
}
}
return array_values(array_filter(array_unique($image_styles)));
}
}

View File

@@ -0,0 +1,304 @@
<?php
namespace Drupal\responsive_image\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\LinkGeneratorInterface;
/**
* Plugin for responsive image formatter.
*/
#[FieldFormatter(
id: 'responsive_image',
label: new TranslatableMarkup('Responsive image'),
field_types: [
'image',
],
)]
class ResponsiveImageFormatter extends ImageFormatterBase {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $responsiveImageStyleStorage;
/**
* The image style entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $imageStyleStorage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The link generator.
*
* @var \Drupal\Core\Utility\LinkGeneratorInterface
*/
protected $linkGenerator;
/**
* Constructs a ResponsiveImageFormatter object.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Entity\EntityStorageInterface $responsive_image_style_storage
* The responsive image style storage.
* @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
* The image style storage.
* @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
* The link generator service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityStorageInterface $responsive_image_style_storage, EntityStorageInterface $image_style_storage, LinkGeneratorInterface $link_generator, AccountInterface $current_user) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->responsiveImageStyleStorage = $responsive_image_style_storage;
$this->imageStyleStorage = $image_style_storage;
$this->linkGenerator = $link_generator;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('entity_type.manager')->getStorage('responsive_image_style'),
$container->get('entity_type.manager')->getStorage('image_style'),
$container->get('link_generator'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'responsive_image_style' => '',
'image_link' => '',
'image_loading' => [
'attribute' => 'lazy',
],
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = parent::settingsForm($form, $form_state);
$responsive_image_options = [];
$responsive_image_styles = $this->responsiveImageStyleStorage->loadMultiple();
uasort($responsive_image_styles, '\Drupal\responsive_image\Entity\ResponsiveImageStyle::sort');
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_style'] = [
'#title' => $this->t('Responsive image style'),
'#type' => 'select',
'#default_value' => $this->getSetting('responsive_image_style') ?: NULL,
'#required' => TRUE,
'#options' => $responsive_image_options,
'#description' => [
'#markup' => $this->linkGenerator->generate($this->t('Configure Responsive Image Styles'), new Url('entity.responsive_image_style.collection')),
'#access' => $this->currentUser->hasPermission('administer responsive image styles'),
],
];
$image_loading = $this->getSetting('image_loading');
$elements['image_loading'] = [
'#type' => 'details',
'#title' => $this->t('Image loading'),
'#weight' => 10,
'#description' => $this->t('Lazy render images with native image loading attribute (<em>loading="lazy"</em>). This improves performance by allowing browsers to lazily load images. See <a href="@url">Lazy loading</a>.', [
'@url' => 'https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes',
]),
];
$loading_attribute_options = [
'lazy' => $this->t('Lazy'),
'eager' => $this->t('Eager'),
];
$elements['image_loading']['attribute'] = [
'#title' => $this->t('Lazy loading attribute'),
'#type' => 'select',
'#default_value' => $image_loading['attribute'],
'#options' => $loading_attribute_options,
'#description' => $this->t('Select the lazy loading attribute for images. <a href=":link">Learn more.</a>', [
':link' => 'https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-loading-attributes',
]),
];
$link_types = [
'content' => $this->t('Content'),
'file' => $this->t('File'),
];
$elements['image_link'] = [
'#title' => $this->t('Link image to'),
'#type' => 'select',
'#default_value' => $this->getSetting('image_link'),
'#empty_option' => $this->t('Nothing'),
'#options' => $link_types,
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$responsive_image_style = $this->responsiveImageStyleStorage->load($this->getSetting('responsive_image_style'));
if ($responsive_image_style) {
$summary[] = $this->t('Responsive image style: @responsive_image_style', ['@responsive_image_style' => $responsive_image_style->label()]);
$link_types = [
'content' => $this->t('Linked to content'),
'file' => $this->t('Linked to file'),
];
// Display this setting only if image is linked.
if (isset($link_types[$this->getSetting('image_link')])) {
$summary[] = $link_types[$this->getSetting('image_link')];
}
}
else {
$summary[] = $this->t('Select a responsive image style.');
}
$image_loading = $this->getSetting('image_loading');
$summary[] = $this->t('Loading attribute: @attribute', [
'@attribute' => $image_loading['attribute'],
]);
return array_merge($summary, parent::settingsSummary());
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$files = $this->getEntitiesToView($items, $langcode);
// Early opt-out if the field is empty.
if (empty($files)) {
return $elements;
}
$url = NULL;
// Check if the formatter involves a link.
if ($this->getSetting('image_link') == 'content') {
$entity = $items->getEntity();
if (!$entity->isNew()) {
$url = $entity->toUrl();
}
}
elseif ($this->getSetting('image_link') == 'file') {
$link_file = TRUE;
}
// Collect cache tags to be added for each item in the field.
$responsive_image_style = $this->responsiveImageStyleStorage->load($this->getSetting('responsive_image_style'));
$image_styles_to_load = [];
$cache_tags = [];
if ($responsive_image_style) {
$cache_tags = Cache::mergeTags($cache_tags, $responsive_image_style->getCacheTags());
$image_styles_to_load = $responsive_image_style->getImageStyleIds();
}
$image_styles = $this->imageStyleStorage->loadMultiple($image_styles_to_load);
foreach ($image_styles as $image_style) {
$cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags());
}
foreach ($files as $delta => $file) {
assert($file instanceof FileInterface);
// Link the <picture> element to the original file.
if (isset($link_file)) {
$url = $file->createFileUrl();
}
// Extract field item attributes for the theme function, and unset them
// from the $item so that the field template does not re-render them.
$item = $file->_referringItem;
$item_attributes = $item->_attributes;
unset($item->_attributes);
$image_loading_settings = $this->getSetting('image_loading');
$item_attributes['loading'] = $image_loading_settings['attribute'];
$elements[$delta] = [
'#theme' => 'responsive_image_formatter',
'#item' => $item,
'#item_attributes' => $item_attributes,
'#responsive_image_style_id' => $responsive_image_style ? $responsive_image_style->id() : '',
'#url' => $url,
'#cache' => [
'tags' => $cache_tags,
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$style_id = $this->getSetting('responsive_image_style');
/** @var \Drupal\responsive_image\ResponsiveImageStyleInterface $style */
if ($style_id && $style = ResponsiveImageStyle::load($style_id)) {
// Add the responsive image style as dependency.
$dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName();
}
return $dependencies;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Drupal\responsive_image\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Transforms image style mappings.
*/
#[MigrateProcess('image_style_mappings')]
class ImageStyleMappings extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!is_array($value)) {
throw new MigrateException('Input should be an array');
}
[$mappings, $breakpoint_group] = $value;
$new_value = [];
foreach ($mappings as $mapping_id => $mapping) {
// The id is in the key with the form
// "breakpoints.theme.my_theme_id.image_style_machine_name". We want the
// identifier after the last period.
preg_match('/\.([a-z0-9_]+)$/', $mapping_id, $matches);
foreach ($mapping as $multiplier => $multiplier_settings) {
if ($multiplier_settings['mapping_type'] == '_none') {
continue;
}
$image_style = [
'breakpoint_id' => $breakpoint_group . '.' . $matches[1],
'multiplier' => $multiplier,
'image_mapping_type' => $multiplier_settings['mapping_type'],
'image_mapping' => $this->getMultiplierSettings($multiplier_settings),
];
$new_value[] = $image_style;
}
}
return $new_value;
}
/**
* Extracts multiplier settings based on its type.
*
* @param array[] $multiplier_settings
* The multiplier settings.
*
* @return array
* The multiplier settings.
*/
protected function getMultiplierSettings(array $multiplier_settings) {
$settings = [];
if ($multiplier_settings['mapping_type'] == 'image_style') {
$settings = $multiplier_settings['image_style'];
}
elseif ($multiplier_settings['mapping_type'] == 'sizes') {
$settings = [
'sizes' => $multiplier_settings['sizes'],
'sizes_image_styles' => array_values($multiplier_settings['sizes_image_styles']),
];
}
return $settings;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\responsive_image\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Gets Drupal responsive image styles source from database.
*
* Breakpoints are YAML files in Drupal 8. If you have a custom
* theme and want to migrate its responsive image styles to
* Drupal 8, create the respective your_theme.breakpoints.yml file at
* the root of the theme.
*
* @see https://www.drupal.org/docs/8/theming-drupal-8/working-with-breakpoints-in-drupal-8
*
* @MigrateSource(
* id = "d7_responsive_image_styles",
* source_module = "picture"
* )
*/
class ResponsiveImageStyles extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('picture_mapping', 'p')
->fields('p');
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'label' => $this->t('The human-readable name of the mapping'),
'machine_name' => $this->t('The machine name of the mapping'),
'breakpoint_group' => $this->t('The group this mapping belongs to'),
'mapping' => $this->t('The mappings linked to the breakpoints group'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['machine_name']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$row->setSourceProperty('mapping', unserialize($row->getSourceProperty('mapping')));
return parent::prepareRow($row);
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\responsive_image;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
/**
* Provides a BC layer for modules providing old configurations.
*
* @internal
* This class is only meant to fix outdated responsive image configuration and
* its methods should not be invoked directly.
*/
final class ResponsiveImageConfigUpdater {
/**
* Flag determining whether deprecations should be triggered.
*
* @var bool
*/
private $deprecationsEnabled = TRUE;
/**
* Stores which deprecations were triggered.
*
* @var bool
*/
private $triggeredDeprecations = [];
/**
* Sets the deprecations enabling status.
*
* @param bool $enabled
* Whether deprecations should be enabled.
*/
public function setDeprecationsEnabled(bool $enabled): void {
$this->deprecationsEnabled = $enabled;
}
/**
* Re-order mappings by breakpoint ID and descending numeric multiplier order.
*
* @param \Drupal\responsive_image\ResponsiveImageStyleInterface $responsive_image_style
* The responsive image style
*
* @return bool
* Whether the responsive image style was updated.
*
* @todo when removing this, evaluate if we need to keep it permanently
* to support an upgrade path (migration) from Drupal 7 picture module.
*/
public function orderMultipliersNumerically(ResponsiveImageStyleInterface $responsive_image_style): bool {
$changed = FALSE;
$original_mapping_order = $responsive_image_style->getImageStyleMappings();
$responsive_image_style->removeImageStyleMappings();
foreach ($original_mapping_order as $mapping) {
$responsive_image_style->addImageStyleMapping($mapping['breakpoint_id'], $mapping['multiplier'], $mapping);
}
if ($responsive_image_style->getImageStyleMappings() !== $original_mapping_order) {
$changed = TRUE;
}
$deprecations_triggered = &$this->triggeredDeprecations['3267870'][$responsive_image_style->id()];
if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) {
$deprecations_triggered = TRUE;
@trigger_error(sprintf('The responsive image style multiplier re-ordering update for "%s" is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Profile, module and theme provided Responsive Image configuration should be updated. See https://www.drupal.org/node/3274803', $responsive_image_style->id()), E_USER_DEPRECATED);
}
return $changed;
}
/**
* Processes responsive image type fields.
*
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display
* The view display.
*
* @return bool
* Whether the display was updated.
*/
public function processResponsiveImageField(EntityViewDisplayInterface $view_display): bool {
$changed = FALSE;
foreach ($view_display->getComponents() as $field => $component) {
if (isset($component['type'])
&& $component['type'] === 'responsive_image'
&& !array_key_exists('image_loading', $component['settings'])
) {
$component['settings']['image_loading']['attribute'] = 'eager';
$view_display->setComponent($field, $component);
$changed = TRUE;
}
}
$deprecations_triggered = &$this->triggeredDeprecations['3192234'][$view_display->id()];
if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) {
$deprecations_triggered = TRUE;
@trigger_error(sprintf('The responsive image loading attribute update for "%s" is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Configuration should be updated. See https://www.drupal.org/node/3279032', $view_display->id()), E_USER_DEPRECATED);
}
return $changed;
}
}

View File

@@ -0,0 +1,296 @@
<?php
namespace Drupal\responsive_image;
use Drupal\Core\Url;
use Drupal\breakpoint\BreakpointManagerInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the responsive image edit/add forms.
*
* @internal
*/
class ResponsiveImageStyleForm extends EntityForm {
/**
* The breakpoint manager.
*
* @var \Drupal\breakpoint\BreakpointManagerInterface
*/
protected $breakpointManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->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('<em>Duplicate responsive image style</em> @label', ['@label' => $this->entity->label()]);
$this->entity = $this->entity->createDuplicate();
}
if ($this->operation == 'edit') {
$form['#title'] = $this->t('<em>Edit responsive image style</em> @label', ['@label' => $this->entity->label()]);
}
/** @var \Drupal\responsive_image\ResponsiveImageStyleInterface $responsive_image_style */
$responsive_image_style = $this->entity;
$form['label'] = [
'#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'] = [
'#type' => 'machine_name',
'#default_value' => $responsive_image_style->id(),
'#machine_name' => [
'exists' => '\Drupal\responsive_image\Entity\ResponsiveImageStyle::load',
'source' => ['label'],
],
'#disabled' => (bool) $responsive_image_style->id() && $this->operation != 'duplicate',
];
$image_styles = image_style_options(TRUE);
$image_styles[ResponsiveImageStyleInterface::ORIGINAL_IMAGE] = $this->t('- None (original image) -');
$image_styles[ResponsiveImageStyleInterface::EMPTY_IMAGE] = $this->t('- empty image -');
if ((bool) $responsive_image_style->id() && $this->operation != 'duplicate') {
$description = $this->t('Select a breakpoint group from the installed themes and modules. Below you can select which breakpoints to use from this group. You can also select which image style or styles to use for each breakpoint you use.') . ' ' . $this->t("Warning: if you change the breakpoint group you lose all your image style selections for each breakpoint.");
}
else {
$description = $this->t('Select a breakpoint group from the installed themes and modules.');
}
$form['breakpoint_group'] = [
'#type' => 'select',
'#title' => $this->t('Breakpoint group'),
'#default_value' => $responsive_image_style->getBreakpointGroup() ?: 'responsive_image',
'#options' => $this->breakpointManager->getGroups(),
'#required' => TRUE,
'#description' => $description,
'#ajax' => [
'callback' => '::breakpointMappingFormAjax',
'wrapper' => 'responsive-image-style-breakpoints-wrapper',
],
];
$form['keyed_styles'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'responsive-image-style-breakpoints-wrapper',
],
];
// By default, breakpoints are ordered from smallest weight to largest:
// the smallest weight is expected to have the smallest breakpoint width,
// while the largest weight is expected to have the largest breakpoint
// width. For responsive images, we need largest breakpoint widths first, so
// we need to reverse the order of these breakpoints.
$breakpoints = array_reverse($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] = [
'#type' => 'details',
'#title' => $label,
];
$image_style_mapping = $responsive_image_style->getImageStyleMapping($breakpoint_id, $multiplier);
if (\Drupal::moduleHandler()->moduleExists('help')) {
$description = $this->t('See the <a href=":responsive_image_help">Responsive Image help page</a> for information on the sizes attribute.', [':responsive_image_help' => Url::fromRoute('help.page', ['name' => 'responsive_image'])->toString()]);
}
else {
$description = $this->t('Install the Help module for more information on the sizes attribute.');
}
$form['keyed_styles'][$breakpoint_id][$multiplier]['image_mapping_type'] = [
'#title' => $this->t('Type'),
'#type' => 'radios',
'#options' => [
'sizes' => $this->t('Select multiple image styles and use the sizes attribute.'),
'image_style' => $this->t('Select a single image style.'),
'_none' => $this->t('Do not use this breakpoint.'),
],
'#default_value' => $image_style_mapping['image_mapping_type'] ?? '_none',
'#description' => $description,
];
$form['keyed_styles'][$breakpoint_id][$multiplier]['image_style'] = [
'#type' => 'select',
'#title' => $this->t('Image style'),
'#options' => $image_styles,
'#default_value' => isset($image_style_mapping['image_mapping']) && is_string($image_style_mapping['image_mapping']) ? $image_style_mapping['image_mapping'] : '',
'#description' => $this->t('Select an image style for this breakpoint.'),
'#states' => [
'visible' => [
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => ['value' => 'image_style'],
],
],
];
$form['keyed_styles'][$breakpoint_id][$multiplier]['sizes'] = [
'#type' => 'textarea',
'#title' => $this->t('Sizes'),
'#default_value' => $image_style_mapping['image_mapping']['sizes'] ?? '100vw',
'#description' => $this->t('Enter the value for the sizes attribute, for example: %example_sizes.', ['%example_sizes' => '(min-width:700px) 700px, 100vw']),
'#states' => [
'visible' => [
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => ['value' => 'sizes'],
],
'required' => [
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => ['value' => 'sizes'],
],
],
];
$form['keyed_styles'][$breakpoint_id][$multiplier]['sizes_image_styles'] = [
'#title' => $this->t('Image styles'),
'#type' => 'checkboxes',
'#options' => array_diff_key($image_styles, ['' => '']),
'#description' => $this->t('Select image styles with widths that range from the smallest amount of space this image will take up in the layout to the largest, bearing in mind that high resolution screens will need images 1.5x to 2x larger.'),
'#default_value' => $image_style_mapping['image_mapping']['sizes_image_styles'] ?? [],
'#states' => [
'visible' => [
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => ['value' => 'sizes'],
],
'required' => [
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => ['value' => 'sizes'],
],
],
];
// Expand the details if "do not use this breakpoint" was not selected.
if ($form['keyed_styles'][$breakpoint_id][$multiplier]['image_mapping_type']['#default_value'] != '_none') {
$form['keyed_styles'][$breakpoint_id][$multiplier]['#open'] = TRUE;
}
}
}
$form['fallback_image_style'] = [
'#title' => $this->t('Fallback image style'),
'#type' => 'select',
'#default_value' => $responsive_image_style->getFallbackImageStyle(),
'#options' => $image_styles,
'#required' => TRUE,
'#description' => $this->t('Select the image style you wish to use as the style when a browser does not support responsive images.'),
];
$form['#tree'] = TRUE;
return parent::form($form, $form_state);
}
/**
* Get the form for mapping breakpoints to image styles.
*/
public function breakpointMappingFormAjax($form, FormStateInterface $form_state) {
return $form['keyed_styles'];
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $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');
return;
}
// Check that at least 1 image style has been selected when using sizes.
foreach ($form_state->getValue('keyed_styles') as $breakpoint_id => $multipliers) {
foreach ($multipliers as $multiplier => $image_style_mapping) {
if ($image_style_mapping['image_mapping_type'] === 'sizes') {
if (empty($image_style_mapping['sizes'])) {
$form_state->setError($form['keyed_styles'][$breakpoint_id][$multiplier]['sizes'], 'Provide a value for the sizes attribute.');
}
if (empty(array_keys(array_filter($image_style_mapping['sizes_image_styles'])))) {
$form_state->setError($form['keyed_styles'][$breakpoint_id][$multiplier]['sizes_image_styles'], 'Select at least one image style.');
}
}
}
}
}
}
/**
* {@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) {
if ($image_style_mapping['image_mapping_type'] === 'sizes') {
$mapping = [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => $image_style_mapping['sizes'],
'sizes_image_styles' => array_keys(array_filter($image_style_mapping['sizes_image_styles'])),
],
];
$responsive_image_style->addImageStyleMapping($breakpoint_id, $multiplier, $mapping);
}
elseif ($image_style_mapping['image_mapping_type'] === 'image_style') {
$mapping = [
'image_mapping_type' => 'image_style',
'image_mapping' => $image_style_mapping['image_style'],
];
$responsive_image_style->addImageStyleMapping($breakpoint_id, $multiplier, $mapping);
}
}
}
}
$responsive_image_style->save();
$this->logger('responsive_image')->notice('Responsive image style @label saved.', ['@label' => $responsive_image_style->label()]);
$this->messenger()->addStatus($this->t('Responsive image style %label saved.', ['%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',
['responsive_image_style' => $responsive_image_style->id()]
);
}
else {
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
}
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Drupal\responsive_image;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a responsive_image mapping entity.
*/
interface ResponsiveImageStyleInterface extends ConfigEntityInterface {
/**
* The machine name for the empty image breakpoint image style option.
*/
const EMPTY_IMAGE = '_empty image_';
/**
* The machine name for the original image breakpoint image style option.
*/
const ORIGINAL_IMAGE = '_original image_';
/**
* Checks if there is at least one mapping defined.
*
* @return bool
* Whether the entity has any image style mappings.
*/
public function hasImageStyleMappings();
/**
* Returns the mappings of breakpoint ID and multiplier to image style.
*
* @return array[]
* The image style mappings. Keyed by breakpoint ID then multiplier.
* The value is the image style mapping array with following keys:
* - image_mapping_type: Either 'image_style' or 'sizes'.
* - image_mapping:
* - If image_mapping_type is 'image_style', the image style ID.
* - If image_mapping_type is 'sizes', an array with following keys:
* - sizes: The value for the 'sizes' attribute.
* - sizes_image_styles: The image styles to use for the 'srcset'
* attribute.
* - breakpoint_id: The breakpoint ID for this mapping.
* - multiplier: The multiplier for this mapping.
*/
public function getKeyedImageStyleMappings();
/**
* Returns the image style mappings for the responsive image style.
*
* @return array[]
* An array of image style mappings. Each image style mapping array
* contains the following keys:
* - breakpoint_id
* - multiplier
* - image_mapping_type
* - image_mapping
*/
public function getImageStyleMappings();
/**
* Sets the breakpoint group for the responsive image style.
*
* @param string $breakpoint_group
* The responsive image style breakpoint group.
*
* @return $this
*/
public function setBreakpointGroup($breakpoint_group);
/**
* Returns the breakpoint group for the responsive image style.
*
* @return string
* The breakpoint group.
*/
public function getBreakpointGroup();
/**
* Sets the fallback image style for the responsive image style.
*
* @param string $fallback_image_style
* The fallback image style ID.
*
* @return $this
*/
public function setFallbackImageStyle($fallback_image_style);
/**
* Returns the fallback image style ID for the responsive image style.
*
* @return string
* The fallback image style ID.
*/
public function getFallbackImageStyle();
/**
* Gets the image style mapping for a breakpoint ID and multiplier.
*
* @param string $breakpoint_id
* The breakpoint ID.
* @param string $multiplier
* The multiplier.
*
* @return array|null
* The image style mapping. NULL if the mapping does not exist.
* The image style mapping has following keys:
* - image_mapping_type: Either 'image_style' or 'sizes'.
* - image_mapping:
* - If image_mapping_type is 'image_style', the image style ID.
* - If image_mapping_type is 'sizes', an array with following keys:
* - sizes: The value for the 'sizes' attribute.
* - sizes_image_styles: The image styles to use for the 'srcset'
* attribute.
* - breakpoint_id: The breakpoint ID for this image style mapping.
* - multiplier: The multiplier for this image style mapping.
*/
public function getImageStyleMapping($breakpoint_id, $multiplier);
/**
* Checks if there is at least one image style mapping defined.
*
* @param array $image_style_mapping
* The image style mapping.
*
* @return bool
* Whether the image style mapping is empty.
*/
public static function isEmptyImageStyleMapping(array $image_style_mapping);
/**
* Adds an image style mapping to the responsive image configuration entity.
*
* @param string $breakpoint_id
* The breakpoint ID.
* @param string $multiplier
* The multiplier.
* @param array $image_style_mapping
* The mapping image style mapping.
*
* @return $this
*/
public function addImageStyleMapping($breakpoint_id, $multiplier, array $image_style_mapping);
/**
* Removes all image style mappings from the responsive image style.
*
* @return $this
*/
public function removeImageStyleMappings();
/**
* Gets all the image styles IDs involved in the responsive image mapping.
*
* @return string[]
*/
public function getImageStyleIds();
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\responsive_image;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a listing of responsive image styles.
*/
class ResponsiveImageStyleListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = t('Label');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
$operations['duplicate'] = [
'title' => t('Duplicate'),
'weight' => 15,
'url' => $entity->toUrl('duplicate-form'),
];
return $operations;
}
}

View File

@@ -0,0 +1,19 @@
{#
/**
* @file
* Default theme implementation to display a formatted responsive image field.
*
* Available variables:
* - responsive_image: A collection of responsive image data.
* - url: An optional URL the image can be linked to.
*
* @see template_preprocess_responsive_image_formatter()
*
* @ingroup themeable
*/
#}
{% if url %}
<a href="{{ url }}">{{ responsive_image }}</a>
{% else %}
{{ responsive_image }}
{% endif %}

View File

@@ -0,0 +1,30 @@
{#
/**
* @file
* Default theme implementation of a responsive image.
*
* Available variables:
* - sources: The attributes of the <source> tags for this <picture> tag.
* - img_element: The controlling image, with the fallback image in srcset.
* - output_image_tag: Whether or not to output an <img> tag instead of a
* <picture> tag.
*
* @see template_preprocess()
* @see template_preprocess_responsive_image()
*
* @ingroup themeable
*/
#}
{% if output_image_tag %}
{{ img_element }}
{% else %}
<picture>
{% if sources %}
{% for source_attributes in sources %}
<source{{ source_attributes }}/>
{% endfor %}
{% endif %}
{# The controlling image, with the fallback image in srcset. #}
{{ img_element }}
</picture>
{% endif %}

View File

@@ -0,0 +1,70 @@
<?php
/**
* @file
* Test lazy load update by modifying an image field form display.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Add a responsive image style.
$styles = [];
$styles['langcode'] = 'en';
$styles['status'] = TRUE;
$styles['dependencies']['config'][] = 'image.style.large';
$styles['dependencies']['config'][] = 'image.style.medium';
$styles['dependencies']['config'][] = 'image.style.thumbnail';
$styles['id'] = 'responsive_image_style';
$styles['uuid'] = '46225242-eb4c-4b10-9a8c-966130b18630';
$styles['label'] = 'Responsive Image Style';
$styles['breakpoint_group'] = 'responsive_image';
$styles['fallback_image_style'] = 'medium';
$styles['image_style_mappings'] = [
[
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '100vw',
'sizes_image_styles' => [
'large',
'medium',
'thumbnail',
],
],
'breakpoint_id' => 'responsive_image.viewport_sizing',
'multiplier' => '1x',
],
];
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'responsive_image.styles.responsive_image_style',
'data' => serialize($styles),
])
->execute();
// Update article view display to use responsive_image.
$article_form_display = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.article.default')
->execute()
->fetchField();
$article_form_display = unserialize($article_form_display);
$article_form_display['content']['field_image']['type'] = 'responsive_image';
$article_form_display['content']['field_image']['settings'] = [
'responsive_image_style' => 'responsive_image_style',
'image_link' => '',
];
$connection->update('config')
->fields(['data' => serialize($article_form_display)])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.article.default')
->execute();

View File

@@ -0,0 +1,71 @@
<?php
/**
* @file
* Test fixture for re-ordering responsive image style multipliers numerically.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Add a responsive image style.
$styles = [];
$styles['langcode'] = 'en';
$styles['status'] = TRUE;
$styles['dependencies']['config'][] = 'image.style.large';
$styles['dependencies']['config'][] = 'image.style.medium';
$styles['dependencies']['config'][] = 'image.style.thumbnail';
$styles['id'] = 'responsive_image_style';
$styles['uuid'] = '46225242-eb4c-4b10-9a8c-966130b18630';
$styles['label'] = 'Responsive Image Style';
$styles['breakpoint_group'] = 'responsive_image';
$styles['fallback_image_style'] = 'medium';
$styles['image_style_mappings'] = [
[
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '75vw',
'sizes_image_styles' => [
'medium',
],
],
'breakpoint_id' => 'responsive_image.viewport_sizing',
'multiplier' => '1.5x',
],
[
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '100vw',
'sizes_image_styles' => [
'large',
],
],
'breakpoint_id' => 'responsive_image.viewport_sizing',
'multiplier' => '2x',
],
[
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '50vw',
'sizes_image_styles' => [
'thumbnail',
],
],
'breakpoint_id' => 'responsive_image.viewport_sizing',
'multiplier' => '1x',
],
];
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'responsive_image.styles.responsive_image_style',
'data' => serialize($styles),
])
->execute();

View File

@@ -0,0 +1,55 @@
<?php
/**
* @file
* Test fixture.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->fields([
'value' => 'i:8000;',
'name' => 'responsive_image',
'collection' => 'system.schema',
])
->condition('collection', 'system.schema')
->condition('name', 'responsive_image')
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['responsive_image'] = 0;
$connection->update('config')
->fields(['data' => serialize($extensions)])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
// Add all responsive_image_removed_post_updates() as existing updates.
require_once __DIR__ . '/../../../../responsive_image/responsive_image.post_update.php';
$existing_updates = $connection->select('key_value')
->fields('key_value', ['value'])
->condition('collection', 'post_update')
->condition('name', 'existing_updates')
->execute()
->fetchField();
$existing_updates = unserialize($existing_updates);
$existing_updates = array_merge(
$existing_updates,
array_keys(responsive_image_removed_post_updates())
);
$connection->update('key_value')
->fields(['value' => serialize($existing_updates)])
->condition('collection', 'post_update')
->condition('name', 'existing_updates')
->execute();

View File

@@ -0,0 +1,33 @@
responsive_image_test_module.empty:
label: empty
mediaQuery: ''
weight: 0
multipliers:
- 1x
- 1.5x
- 2x
responsive_image_test_module.mobile:
label: mobile
mediaQuery: '(min-width: 0px)'
weight: 1
multipliers:
- 1x
- 1.5x
- 2x
responsive_image_test_module.narrow:
label: narrow
mediaQuery: '(min-width: 560px)'
weight: 2
multipliers:
- 1x
- 1.5x
- 2x
responsive_image_test_module.wide:
label: wide
mediaQuery: '(min-width: 851px)'
weight: 3
multipliers:
- 1x
- 1.5x
- 2x

View File

@@ -0,0 +1,10 @@
name: 'Responsive image test theme'
type: module
description: 'Test theme for responsive image.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\responsive_image_test_module\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\responsive_image\Plugin\Field\FieldFormatter\ResponsiveImageFormatter;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin to test responsive image formatter.
*/
#[FieldFormatter(
id: 'responsive_image_test',
label: new TranslatableMarkup('Responsive image test'),
field_types: [
'image',
],
)]
class ResponsiveImageTestFormatter extends ResponsiveImageFormatter {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = parent::viewElements($items, $langcode);
// Unset #item_attributes to test that the theme function can handle that.
foreach ($elements as &$element) {
if (isset($element['#item_attributes'])) {
unset($element['#item_attributes']);
}
}
return $elements;
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for responsive_image.
*
* @group responsive_image
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional;
use Drupal\responsive_image\ResponsiveImageStyleInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Thoroughly test the administrative interface of the Responsive Image module.
*
* @group responsive_image
*/
class ResponsiveImageAdminUITest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'responsive_image',
'responsive_image_test_module',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'administer responsive images',
]));
}
/**
* Tests responsive image administration functionality.
*/
public function testResponsiveImageAdmin(): void {
// We start without any default styles.
$this->drupalGet('admin/config/media/responsive-image-style');
$this->assertSession()->pageTextContains('There are no responsive image styles yet.');
// Add a responsive image style.
$this->drupalGet('admin/config/media/responsive-image-style/add');
// The 'Responsive Image' breakpoint group should be selected by default.
$this->assertSession()->fieldValueEquals('breakpoint_group', 'responsive_image');
// Create a new group.
$edit = [
'label' => 'Style One',
'id' => 'style_one',
'breakpoint_group' => 'responsive_image',
'fallback_image_style' => 'thumbnail',
];
$this->drupalGet('admin/config/media/responsive-image-style/add');
$this->submitForm($edit, 'Save');
// Check if the new group is created.
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('admin/config/media/responsive-image-style');
$this->assertSession()->pageTextNotContains('There are no responsive image styles yet.');
$this->assertSession()->pageTextContains('Style One');
// Edit the breakpoint_group.
$this->drupalGet('admin/config/media/responsive-image-style/style_one');
$this->assertSession()->fieldValueEquals('label', 'Style One');
$this->assertSession()->fieldValueEquals('breakpoint_group', 'responsive_image');
$edit = [
'breakpoint_group' => 'responsive_image_test_module',
];
$this->submitForm($edit, 'Save');
// Edit the group.
$this->drupalGet('admin/config/media/responsive-image-style/style_one');
$this->assertSession()->fieldValueEquals('label', 'Style One');
$this->assertSession()->fieldValueEquals('breakpoint_group', 'responsive_image_test_module');
$this->assertSession()->fieldValueEquals('fallback_image_style', 'thumbnail');
$cases = [
['mobile', '1x'],
['mobile', '2x'],
['narrow', '1x'],
['narrow', '2x'],
['wide', '1x'],
['wide', '2x'],
];
$image_styles = array_merge(
[ResponsiveImageStyleInterface::EMPTY_IMAGE, ResponsiveImageStyleInterface::ORIGINAL_IMAGE],
array_keys(image_style_options(FALSE))
);
foreach ($cases as $case) {
// Check if the radio buttons are present.
$this->assertSession()->fieldExists('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_mapping_type]');
// Check if the image style dropdowns are present.
$this->assertSession()->fieldExists('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_style]');
// Check if the sizes textfields are present.
$this->assertSession()->fieldExists('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][sizes]');
foreach ($image_styles as $image_style_name) {
// Check if the image styles are available in the dropdowns.
$this->assertSession()->optionExists('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_style]', $image_style_name);
// Check if the image styles checkboxes are present.
$this->assertSession()->fieldExists('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][sizes_image_styles][' . $image_style_name . ']');
}
}
// Save styles for 1x variant only.
$edit = [
'label' => 'Style One',
'breakpoint_group' => 'responsive_image_test_module',
'fallback_image_style' => 'thumbnail',
'keyed_styles[responsive_image_test_module.mobile][1x][image_mapping_type]' => 'image_style',
'keyed_styles[responsive_image_test_module.mobile][1x][image_style]' => 'thumbnail',
'keyed_styles[responsive_image_test_module.narrow][1x][image_mapping_type]' => 'sizes',
// Ensure the Sizes field allows long values.
'keyed_styles[responsive_image_test_module.narrow][1x][sizes]' => '(min-resolution: 192dpi) and (min-width: 170px) 386px, (min-width: 170px) 193px, (min-width: 768px) 18vw, (min-width: 480px) 30vw, 48vw',
'keyed_styles[responsive_image_test_module.narrow][1x][sizes_image_styles][large]' => 'large',
'keyed_styles[responsive_image_test_module.narrow][1x][sizes_image_styles][medium]' => 'medium',
'keyed_styles[responsive_image_test_module.wide][1x][image_mapping_type]' => 'image_style',
'keyed_styles[responsive_image_test_module.wide][1x][image_style]' => 'large',
];
$this->drupalGet('admin/config/media/responsive-image-style/style_one');
$this->submitForm($edit, 'Save');
$this->drupalGet('admin/config/media/responsive-image-style/style_one');
// Check the mapping for multipliers 1x and 2x for the mobile breakpoint.
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.mobile][1x][image_style]', 'thumbnail');
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.mobile][1x][image_mapping_type]', 'image_style');
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.mobile][2x][image_mapping_type]', '_none');
// Check the mapping for multipliers 1x and 2x for the narrow breakpoint.
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.narrow][1x][image_mapping_type]', 'sizes');
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.narrow][1x][sizes]', '(min-resolution: 192dpi) and (min-width: 170px) 386px, (min-width: 170px) 193px, (min-width: 768px) 18vw, (min-width: 480px) 30vw, 48vw');
$this->assertSession()->checkboxChecked('edit-keyed-styles-responsive-image-test-modulenarrow-1x-sizes-image-styles-large');
$this->assertSession()->checkboxChecked('edit-keyed-styles-responsive-image-test-modulenarrow-1x-sizes-image-styles-medium');
$this->assertSession()->checkboxNotChecked('edit-keyed-styles-responsive-image-test-modulenarrow-1x-sizes-image-styles-thumbnail');
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.narrow][2x][image_mapping_type]', '_none');
// Check the mapping for multipliers 1x and 2x for the wide breakpoint.
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.wide][1x][image_style]', 'large');
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.wide][1x][image_mapping_type]', 'image_style');
$this->assertSession()->fieldValueEquals('keyed_styles[responsive_image_test_module.wide][2x][image_mapping_type]', '_none');
// Delete the style.
$this->drupalGet('admin/config/media/responsive-image-style/style_one/delete');
$this->submitForm([], 'Delete');
$this->drupalGet('admin/config/media/responsive-image-style');
$this->assertSession()->pageTextContains('There are no responsive image styles yet.');
}
}

View File

@@ -0,0 +1,583 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional;
use Drupal\image\Entity\ImageStyle;
use Drupal\image\ImageStyleInterface;
use Drupal\node\Entity\Node;
use Drupal\file\Entity\File;
use Drupal\responsive_image\Plugin\Field\FieldFormatter\ResponsiveImageFormatter;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\responsive_image\ResponsiveImageStyleInterface;
use Drupal\Tests\image\Functional\ImageFieldTestBase;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\RoleInterface;
/**
* Tests responsive image display formatter.
*
* @group responsive_image
* @group #slow
*/
class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase {
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Responsive image style entity instance we test with.
*
* @var \Drupal\responsive_image\Entity\ResponsiveImageStyle
*/
protected $responsiveImgStyle;
/**
* The file URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'field_ui',
'responsive_image',
'responsive_image_test_module',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->fileUrlGenerator = $this->container->get('file_url_generator');
// Create user.
$this->adminUser = $this->drupalCreateUser([
'administer responsive images',
'access content',
'access administration pages',
'administer site configuration',
'administer content types',
'administer node display',
'administer nodes',
'create article content',
'edit any article content',
'delete any article content',
'administer image styles',
]);
$this->drupalLogin($this->adminUser);
// Add responsive image style.
$this->responsiveImgStyle = ResponsiveImageStyle::create([
'id' => 'style_one',
'label' => 'Style One',
'breakpoint_group' => 'responsive_image_test_module',
'fallback_image_style' => 'large',
]);
}
/**
* Tests responsive image formatters on node display for public files.
*/
public function testResponsiveImageFieldFormattersPublic(): void {
$this->addTestImageStyleMappings();
$this->doTestResponsiveImageFieldFormatters('public');
}
/**
* Tests responsive image formatters on node display for private files.
*/
public function testResponsiveImageFieldFormattersPrivate(): void {
$this->addTestImageStyleMappings();
// Remove access content permission from anonymous users.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, ['access content' => FALSE]);
$this->doTestResponsiveImageFieldFormatters('private');
}
/**
* Tests responsive image formatters when image style is empty.
*/
public function testResponsiveImageFieldFormattersEmptyStyle(): void {
$this->addTestImageStyleMappings(TRUE);
$this->doTestResponsiveImageFieldFormatters('public', TRUE);
}
/**
* Add image style mappings to the responsive image style entity.
*
* @param bool $empty_styles
* If true, the image style mappings will get empty image styles.
*/
protected function addTestImageStyleMappings($empty_styles = FALSE) {
if ($empty_styles) {
$this->responsiveImgStyle
->addImageStyleMapping('responsive_image_test_module.mobile', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => '',
])
->addImageStyleMapping('responsive_image_test_module.narrow', '1x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width: 700px) 700px, 100vw',
'sizes_image_styles' => [],
],
])
->addImageStyleMapping('responsive_image_test_module.wide', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => '',
])
->save();
}
else {
$this->responsiveImgStyle
// Test the output of an empty image.
->addImageStyleMapping('responsive_image_test_module.mobile', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => ResponsiveImageStyleInterface::EMPTY_IMAGE,
])
// Test the output with a 1.5x multiplier.
->addImageStyleMapping('responsive_image_test_module.mobile', '1.5x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
])
// Test the output of the 'sizes' attribute.
->addImageStyleMapping('responsive_image_test_module.narrow', '1x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width: 700px) 700px, 100vw',
'sizes_image_styles' => [
'large',
'medium',
],
],
])
// Test the normal output of mapping to an image style.
->addImageStyleMapping('responsive_image_test_module.wide', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
])
// Test the output of the original image.
->addImageStyleMapping('responsive_image_test_module.wide', '3x', [
'image_mapping_type' => 'image_style',
'image_mapping' => ResponsiveImageStyleInterface::ORIGINAL_IMAGE,
])
->save();
}
}
/**
* Tests responsive image formatters on node display.
*
* If the empty styles param is set, then the function only tests for the
* fallback image style (large).
*
* @param string $scheme
* File scheme to use.
* @param bool $empty_styles
* If true, use an empty string for image style names.
* Defaults to false.
*/
protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles = FALSE) {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$field_name = $this->randomMachineName();
$this->createImageField($field_name, 'node', 'article', ['uri_scheme' => $scheme]);
// Create a new node with an image attached. Make sure we use a large image
// so the scale effects of the image styles always have an effect.
$test_image = current($this->getTestFiles('image', 39325));
// Create alt text for the image.
$alt = $this->randomMachineName();
$nid = $this->uploadNodeImage($test_image, $field_name, 'article', $alt);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
// Test that the default formatter is being used.
$image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
$image = [
'#theme' => 'image',
'#uri' => $image_uri,
'#width' => 360,
'#height' => 240,
'#alt' => $alt,
'#attributes' => ['loading' => 'lazy'],
];
$default_output = str_replace("\n", '', (string) $renderer->renderRoot($image));
$this->assertSession()->responseContains($default_output);
// Test field not being configured. This should not cause a fatal error.
$display_options = [
'type' => 'responsive_image_test',
'settings' => ResponsiveImageFormatter::defaultSettings(),
];
$display = $this->container->get('entity_type.manager')
->getStorage('entity_view_display')
->load('node.article.default');
if (!$display) {
$values = [
'targetEntityType' => 'node',
'bundle' => 'article',
'mode' => 'default',
'status' => TRUE,
];
$display = $this->container->get('entity_type.manager')->getStorage('entity_view_display')->create($values);
}
$display->setComponent($field_name, $display_options)->save();
$this->drupalGet('node/' . $nid);
// Test theme function for responsive image, but using the test formatter.
$display_options = [
'type' => 'responsive_image_test',
'settings' => [
'image_link' => 'file',
'responsive_image_style' => 'style_one',
],
];
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display = $display_repository->getViewDisplay('node', 'article');
$display->setComponent($field_name, $display_options)
->save();
$this->drupalGet('node/' . $nid);
// Use the responsive image formatter linked to file formatter.
$display_options = [
'type' => 'responsive_image',
'settings' => [
'image_link' => 'file',
'responsive_image_style' => 'style_one',
],
];
$display = $display_repository->getViewDisplay('node', 'article');
$display->setComponent($field_name, $display_options)
->save();
$this->drupalGet('node/' . $nid);
// No image style cache tag should be found.
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'image_style:');
$this->assertSession()->responseMatches('/<a(.*?)href="' . preg_quote($this->fileUrlGenerator->generateString($image_uri), '/') . '"(.*?)>\s*<picture/');
// Verify that the image can be downloaded.
$this->assertEquals(file_get_contents($test_image->uri), $this->drupalGet($this->fileUrlGenerator->generateAbsoluteString($image_uri)), 'File was downloaded successfully.');
if ($scheme == 'private') {
// Only verify HTTP headers when using private scheme and the headers are
// sent by Drupal.
$this->assertSession()->responseHeaderEquals('Content-Type', 'image/png');
$this->assertSession()->responseHeaderContains('Cache-Control', 'private');
// Log out and ensure the file cannot be accessed.
$this->drupalLogout();
$this->drupalGet($this->fileUrlGenerator->generateAbsoluteString($image_uri));
$this->assertSession()->statusCodeEquals(403);
// Log in again.
$this->drupalLogin($this->adminUser);
}
// Use the responsive image formatter with a responsive image style.
$display_options['settings']['responsive_image_style'] = 'style_one';
$display_options['settings']['image_link'] = '';
$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->assertSession()->responseContains('/styles/medium/');
// Assert the empty image is present.
$this->assertSession()->responseContains('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
$thumbnail_style = ImageStyle::load('thumbnail');
// Assert the output of the 'srcset' attribute (small multipliers first).
$this->assertSession()->responseContains('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . $this->fileUrlGenerator->transformRelative($thumbnail_style->buildUrl($image_uri)) . ' 1.5x');
$this->assertSession()->responseContains('/styles/medium/');
// Assert the output of the original image.
$this->assertSession()->responseContains($this->fileUrlGenerator->generateString($image_uri) . ' 3x');
// Assert the output of the breakpoints.
$this->assertSession()->responseContains('media="(min-width: 0px)"');
$this->assertSession()->responseContains('media="(min-width: 560px)"');
// Assert the output of the 'sizes' attribute.
$this->assertSession()->responseContains('sizes="(min-width: 700px) 700px, 100vw"');
$this->assertSession()->responseMatches('/media="\(min-width: 560px\)".+?sizes="\(min-width: 700px\) 700px, 100vw"/');
// Assert the output of the 'srcset' attribute (small images first).
$medium_style = ImageStyle::load('medium');
$this->assertSession()->responseContains($this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri)) . ' 220w, ' . $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri)) . ' 360w');
$this->assertSession()->responseContains('media="(min-width: 851px)"');
// Assert the output of the 'width' attribute.
$this->assertSession()->responseContains('width="360"');
// Assert the output of the 'height' attribute.
$this->assertSession()->responseContains('height="240"');
$this->assertSession()->responseContains('loading="lazy"');
}
$this->assertSession()->responseContains('/styles/large/');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:responsive_image.styles.style_one');
if (!$empty_styles) {
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:image.style.medium');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:image.style.thumbnail');
$this->assertSession()->responseContains('type="image/webp"');
}
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:image.style.large');
// Test the fallback image style. Copy the source image:
$fallback_image = $image;
// Set the fallback image style uri:
$fallback_image['#uri'] = $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri));
// The image.html.twig template has a newline after the <img> tag but
// responsive-image.html.twig doesn't have one after the fallback image, so
// we remove it here.
$default_output = trim((string) $renderer->renderRoot($fallback_image));
$this->assertSession()->responseContains($default_output);
if ($scheme == 'private') {
// Log out and ensure the file cannot be accessed.
$this->drupalLogout();
$this->drupalGet($large_style->buildUrl($image_uri));
$this->assertSession()->statusCodeEquals(403);
$this->assertSession()->responseHeaderNotMatches('X-Drupal-Cache-Tags', '/ image_style\:/');
}
}
/**
* Tests responsive image formatters on node display linked to the file.
*/
public function testResponsiveImageFieldFormattersLinkToFile(): void {
$this->addTestImageStyleMappings();
$this->assertResponsiveImageFieldFormattersLink('file');
}
/**
* Tests responsive image formatters on node display linked to the node.
*/
public function testResponsiveImageFieldFormattersLinkToNode(): void {
$this->addTestImageStyleMappings();
$this->assertResponsiveImageFieldFormattersLink('content');
}
/**
* Tests responsive image formatter on node display with an empty media query.
*/
public function testResponsiveImageFieldFormattersEmptyMediaQuery(): void {
$this->responsiveImgStyle
// Test the output of an empty media query.
->addImageStyleMapping('responsive_image_test_module.empty', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => ResponsiveImageStyleInterface::EMPTY_IMAGE,
])
// Test the output with a 1.5x multiplier.
->addImageStyleMapping('responsive_image_test_module.mobile', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
])
->save();
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$field_name = $this->randomMachineName();
$this->createImageField($field_name, 'node', 'article', ['uri_scheme' => 'public']);
// Create a new node with an image attached.
$test_image = current($this->getTestFiles('image'));
$nid = $this->uploadNodeImage($test_image, $field_name, 'article', $this->randomMachineName());
$node_storage->resetCache([$nid]);
// Use the responsive image formatter linked to file formatter.
$display_options = [
'type' => 'responsive_image',
'settings' => [
'image_link' => '',
'responsive_image_style' => 'style_one',
],
];
$display = \Drupal::service('entity_display.repository')
->getViewDisplay('node', 'article');
$display->setComponent($field_name, $display_options)
->save();
// View the node.
$this->drupalGet('node/' . $nid);
// Assert an empty media attribute is not output.
$this->assertSession()->responseNotMatches('@srcset="data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x".+?media=".+?/><source@');
// Assert the media attribute is present if it has a value.
$thumbnail_style = ImageStyle::load('thumbnail');
$node = $node_storage->load($nid);
$image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
$this->assertSession()->responseMatches('/srcset="' . preg_quote($this->fileUrlGenerator->transformRelative($thumbnail_style->buildUrl($image_uri)), '/') . ' 1x".+?media="\(min-width: 0px\)"/');
}
/**
* Tests responsive image formatter on node display with one and two sources.
*/
public function testResponsiveImageFieldFormattersMultipleSources(): void {
// Setup known image style sizes so the test can assert on known sizes.
$large_style = ImageStyle::load('large');
assert($large_style instanceof ImageStyleInterface);
$large_style->addImageEffect([
'id' => 'image_resize',
'data' => [
'width' => '480',
'height' => '480',
],
]);
$large_style->save();
$medium_style = ImageStyle::load('medium');
assert($medium_style instanceof ImageStyleInterface);
$medium_style->addImageEffect([
'id' => 'image_resize',
'data' => [
'width' => '220',
'height' => '220',
],
]);
$medium_style->save();
$this->responsiveImgStyle
// Test the output of an empty media query.
->addImageStyleMapping('responsive_image_test_module.empty', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => $medium_style->id(),
])
->addImageStyleMapping('responsive_image_test_module.empty', '1.5x', [
'image_mapping_type' => 'image_style',
'image_mapping' => $large_style->id(),
])
->addImageStyleMapping('responsive_image_test_module.empty', '2x', [
'image_mapping_type' => 'image_style',
'image_mapping' => $large_style->id(),
])
->save();
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$field_name = $this->randomMachineName();
$this->createImageField($field_name, 'node', 'article', ['uri_scheme' => 'public']);
// Create a new node with an image attached.
$test_image = current($this->getTestFiles('image'));
$nid = $this->uploadNodeImage($test_image, $field_name, 'article', $this->randomMachineName());
$node_storage->resetCache([$nid]);
// Use the responsive image formatter linked to file formatter.
$display_options = [
'type' => 'responsive_image',
'settings' => [
'image_link' => '',
'responsive_image_style' => 'style_one',
'image_loading' => [
// Test the image loading default option can be overridden.
'attribute' => 'eager',
],
],
];
$display = \Drupal::service('entity_display.repository')
->getViewDisplay('node', 'article');
$display->setComponent($field_name, $display_options)
->save();
// View the node.
$this->drupalGet('node/' . $nid);
// Assert the img tag has medium and large images and fallback dimensions
// from the large image style are used.
$node = $node_storage->load($nid);
$image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
$medium_transform_url = $this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri));
$large_transform_url = $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri));
$this->assertSession()->responseMatches('/<img loading="eager" srcset="' . \preg_quote($medium_transform_url, '/') . ' 1x, ' . \preg_quote($large_transform_url, '/') . ' 1.5x, ' . \preg_quote($large_transform_url, '/') . ' 2x" width="220" height="220" src="' . \preg_quote($large_transform_url, '/') . '" alt="\w+" \/>/');
$this->responsiveImgStyle
// Test the output of an empty media query.
->addImageStyleMapping('responsive_image_test_module.wide', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => $large_style->id(),
])
->save();
// Assert the picture tag has source tags that include dimensions.
$this->drupalGet('node/' . $nid);
$this->assertSession()->responseMatches('/<picture>\s+<source srcset="' . \preg_quote($large_transform_url, '/') . ' 1x" media="\(min-width: 851px\)" type="image\/webp" width="480" height="480"\/>\s+<source srcset="' . \preg_quote($medium_transform_url, '/') . ' 1x, ' . \preg_quote($large_transform_url, '/') . ' 1.5x, ' . \preg_quote($large_transform_url, '/') . ' 2x" type="image\/webp" width="220" height="220"\/>\s+<img loading="eager" src="' . \preg_quote($large_transform_url, '/') . '" width="480" height="480" alt="\w+" \/>\s+<\/picture>/');
}
/**
* Tests responsive image formatters linked to the file or node.
*
* @param string $link_type
* The link type to test. Either 'file' or 'content'.
*/
private function assertResponsiveImageFieldFormattersLink(string $link_type): void {
$field_name = $this->randomMachineName();
$field_settings = ['alt_field_required' => 0];
$this->createImageField($field_name, 'node', 'article', ['uri_scheme' => 'public'], $field_settings);
// Create a new node with an image attached.
$test_image = current($this->getTestFiles('image'));
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Test the image linked to file formatter.
$display_options = [
'type' => 'responsive_image',
'settings' => [
'image_link' => $link_type,
'responsive_image_style' => 'style_one',
],
];
$display_repository->getViewDisplay('node', 'article')
->setComponent($field_name, $display_options)
->save();
// Ensure that preview works.
$this->previewNodeImage($test_image, $field_name, 'article');
// Look for a picture tag in the preview output
$this->assertSession()->responseMatches('/picture/');
$nid = $this->uploadNodeImage($test_image, $field_name, 'article');
$this->container->get('entity_type.manager')->getStorage('node')->resetCache([$nid]);
$node = Node::load($nid);
// Use the responsive image formatter linked to file formatter.
$display_options = [
'type' => 'responsive_image',
'settings' => [
'image_link' => $link_type,
'responsive_image_style' => 'style_one',
],
];
$display_repository->getViewDisplay('node', 'article')
->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->assertSession()->responseMatches('/<a(.*?)href="' . preg_quote($this->fileUrlGenerator->generateString($image_uri), '/') . '"(.*?)>\s*<picture/');
break;
case 'content':
// Make sure the link to the node is present.
$this->assertSession()->responseMatches('/<a(.*?)href="' . preg_quote($node->toUrl()->toString(), '/') . '"(.*?)>\s*<picture/');
break;
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests lazy-load upgrade path.
*
* @coversDefaultClass \Drupal\responsive_image\ResponsiveImageConfigUpdater
*
* @group responsive_image
* @group legacy
*/
class ResponsiveImageLazyLoadUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles(): void {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz',
__DIR__ . '/../../fixtures/update/responsive_image.php',
__DIR__ . '/../../fixtures/update/responsive_image-loading-attribute.php',
];
}
/**
* Test new lazy-load setting upgrade path.
*
* @see responsive_image_post_update_image_loading_attribute
*/
public function testUpdate(): void {
$data = EntityViewDisplay::load('node.article.default')->toArray();
$this->assertArrayNotHasKey('image_loading', $data['content']['field_image']['settings']);
$this->runUpdates();
$data = EntityViewDisplay::load('node.article.default')->toArray();
$this->assertArrayHasKey('image_loading', $data['content']['field_image']['settings']);
$this->assertEquals('eager', $data['content']['field_image']['settings']['image_loading']['attribute']);
}
/**
* Test responsive_image_entity_view_display_presave invokes deprecations.
*
* @covers ::processResponsiveImageField
*/
public function testEntitySave(): void {
$this->expectDeprecation('The responsive image loading attribute update for "node.article.default" is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Configuration should be updated. See https://www.drupal.org/node/3279032');
$view_display = EntityViewDisplay::load('node.article.default');
$this->assertArrayNotHasKey('image_loading', $view_display->toArray()['content']['field_image']['settings']);
$view_display->save();
$view_display = EntityViewDisplay::load('node.article.default');
$this->assertArrayHasKey('image_loading', $view_display->toArray()['content']['field_image']['settings']);
$this->assertEquals('eager', $view_display->toArray()['content']['field_image']['settings']['image_loading']['attribute']);
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
/**
* Tests order multipliers numerically upgrade path.
*
* @coversDefaultClass \Drupal\responsive_image\ResponsiveImageConfigUpdater
*
* @group responsive_image
* @group legacy
*/
class ResponsiveImageOrderMultipliersNumericallyUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles(): void {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz',
__DIR__ . '/../../fixtures/update/responsive_image.php',
__DIR__ . '/../../fixtures/update/responsive_image-order-multipliers-numerically.php',
];
}
/**
* Test order multipliers numerically upgrade path.
*
* @see responsive_image_post_update_order_multiplier_numerically()
*
* @legacy
*/
public function testUpdate(): void {
$mappings = ResponsiveImageStyle::load('responsive_image_style')->getImageStyleMappings();
$this->assertEquals('1.5x', $mappings[0]['multiplier']);
$this->assertEquals('2x', $mappings[1]['multiplier']);
$this->assertEquals('1x', $mappings[2]['multiplier']);
$this->runUpdates();
$mappings = ResponsiveImageStyle::load('responsive_image_style')->getImageStyleMappings();
$this->assertEquals('1x', $mappings[0]['multiplier']);
$this->assertEquals('1.5x', $mappings[1]['multiplier']);
$this->assertEquals('2x', $mappings[2]['multiplier']);
}
/**
* Test ResponsiveImageStyle::preSave correctly orders by multiplier weight.
*
* @covers ::orderMultipliersNumerically
*/
public function testEntitySave(): void {
$this->expectDeprecation('The responsive image style multiplier re-ordering update for "responsive_image_style" is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Profile, module and theme provided Responsive Image configuration should be updated. See https://www.drupal.org/node/3274803');
$image_style = ResponsiveImageStyle::load('responsive_image_style');
$mappings = $image_style->getImageStyleMappings();
$this->assertEquals('1.5x', $mappings[0]['multiplier']);
$this->assertEquals('2x', $mappings[1]['multiplier']);
$this->assertEquals('1x', $mappings[2]['multiplier']);
$image_style->save();
$mappings = ResponsiveImageStyle::load('responsive_image_style')->getImageStyleMappings();
$this->assertEquals('1x', $mappings[0]['multiplier']);
$this->assertEquals('1.5x', $mappings[1]['multiplier']);
$this->assertEquals('2x', $mappings[2]['multiplier']);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class ResponsiveImageStyleJsonAnonTest extends ResponsiveImageStyleResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ResponsiveImageStyleJsonBasicAuthTest extends ResponsiveImageStyleResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class ResponsiveImageStyleJsonCookieTest extends ResponsiveImageStyleResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional\Rest;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
/**
* ResourceTestBase for ResponsiveImageStyle entity.
*/
abstract class ResponsiveImageStyleResourceTestBase extends ConfigEntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['responsive_image'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'responsive_image_style';
/**
* The ResponsiveImageStyle entity.
*
* @var \Drupal\responsive_image\ResponsiveImageStyleInterface
*/
protected $entity;
/**
* The effect UUID.
*
* @var string
*/
protected $effectUuid;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer responsive images']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" responsive image style.
$camelids = ResponsiveImageStyle::create([
'id' => 'camelids',
'label' => 'Camelids',
]);
$camelids->setBreakpointGroup('test_group');
$camelids->setFallbackImageStyle('fallback');
$camelids->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'small',
]);
$camelids->addImageStyleMapping('test_breakpoint', '2x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'medium' => 'medium',
'large' => 'large',
],
],
]);
$camelids->save();
return $camelids;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'breakpoint_group' => 'test_group',
'dependencies' => [
'config' => [
'image.style.large',
'image.style.medium',
],
],
'fallback_image_style' => 'fallback',
'id' => 'camelids',
'image_style_mappings' => [
0 => [
'breakpoint_id' => 'test_breakpoint',
'image_mapping' => 'small',
'image_mapping_type' => 'image_style',
'multiplier' => '1x',
],
1 => [
'breakpoint_id' => 'test_breakpoint',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
'medium' => 'medium',
],
],
'image_mapping_type' => 'sizes',
'multiplier' => '2x',
],
],
'label' => 'Camelids',
'langcode' => 'en',
'status' => TRUE,
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
return [];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'administer responsive images' permission is required.";
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class ResponsiveImageStyleXmlAnonTest extends ResponsiveImageStyleResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class ResponsiveImageStyleXmlBasicAuthTest extends ResponsiveImageStyleResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class ResponsiveImageStyleXmlCookieTest extends ResponsiveImageStyleResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\Tests\views\Functional\ViewTestBase;
/**
* Tests the integration of responsive image with Views.
*
* @group responsive_image
*/
class ViewsIntegrationTest extends ViewTestBase {
/**
* The responsive image style ID to use.
*/
const RESPONSIVE_IMAGE_STYLE_ID = 'responsive_image_style_id';
/**
* {@inheritdoc}
*/
protected static $modules = [
'views',
'views_ui',
'responsive_image',
'field',
'image',
'file',
'entity_test',
'breakpoint',
'responsive_image_test_module',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The test views to enable.
*/
public static $testViews = ['entity_test_row'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['views_test_config']): void {
parent::setUp($import_test_views, $modules);
$this->enableViewsTestModule();
// Create a responsive image style.
$responsive_image_style = ResponsiveImageStyle::create([
'id' => self::RESPONSIVE_IMAGE_STYLE_ID,
'label' => 'Foo',
'breakpoint_group' => 'responsive_image_test_module',
]);
// Create an image field to be used with a responsive image formatter.
FieldStorageConfig::create([
'type' => 'image',
'entity_type' => 'entity_test',
'field_name' => 'bar',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'field_name' => 'bar',
])->save();
$responsive_image_style
->addImageStyleMapping('responsive_image_test_module.mobile', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
])
->addImageStyleMapping('responsive_image_test_module.narrow', '1x', [
'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', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
])
->save();
$admin_user = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($admin_user);
}
/**
* Tests integration with Views.
*/
public function testViewsAddResponsiveImageField(): void {
// Add the image field to the View.
$this->drupalGet('admin/structure/views/nojs/add-handler/entity_test_row/default/field');
$this->drupalGet('admin/structure/views/nojs/add-handler/entity_test_row/default/field');
$this->submitForm(['name[entity_test__bar.bar]' => TRUE], 'Add and configure field');
// Set the formatter to 'Responsive image'.
$this->submitForm(['options[type]' => 'responsive_image'], 'Apply');
$this->assertSession()
->responseContains('Responsive image style field is required.');
$this->submitForm(['options[settings][responsive_image_style]' => self::RESPONSIVE_IMAGE_STYLE_ID], 'Apply');
$this->drupalGet('admin/structure/views/nojs/handler/entity_test_row/default/field/bar');
// Make sure the selected value is set.
$this->assertSession()
->fieldValueEquals('options[settings][responsive_image_style]', self::RESPONSIVE_IMAGE_STYLE_ID);
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
/**
* Tests the responsive image field UI.
*
* @group responsive_image
*/
class ResponsiveImageFieldUiTest extends WebDriverTestBase {
use FieldUiJSTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'field_ui',
'image',
'responsive_image',
'responsive_image_test_module',
'block',
];
/**
* The content type id.
*
* @var string
*/
protected string $type;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'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 = $this->randomMachineName(8) . '_test';
$type = $this->drupalCreateContentType([
'name' => $type_name,
'type' => $type_name,
]);
$this->type = $type->id();
}
/**
* Tests formatter settings.
*/
public function testResponsiveImageFormatterUi(): void {
$manage = 'admin/structure/types/manage/' . $this->type;
$manage_display = $manage . '/display';
/** @var \Drupal\FunctionalJavascriptTests\JSWebAssert $assert_session */
$assert_session = $this->assertSession();
$this->fieldUIAddNewFieldJS('admin/structure/types/manage/' . $this->type, 'image', 'Image', 'image');
// Display the "Manage display" page.
$this->drupalGet($manage_display);
// Change the formatter and check that the summary is updated.
$page = $this->getSession()->getPage();
$field_image_type = $page->findField('fields[field_image][type]');
$field_image_type->setValue('responsive_image');
$summary_text = $assert_session->waitForElement('xpath', $this->cssSelectToXpath('#field-image .ajax-new-content .field-plugin-summary'));
$this->assertEquals('Select a responsive image style. Loading attribute: lazy', $summary_text->getText());
$page->pressButton('Save');
$assert_session->responseContains("Select a responsive image style.");
// Create responsive image styles.
$responsive_image_style = ResponsiveImageStyle::create([
'id' => 'style_one',
'label' => 'Style One',
'breakpoint_group' => 'responsive_image_test_module',
'fallback_image_style' => 'thumbnail',
]);
$responsive_image_style
->addImageStyleMapping('responsive_image_test_module.mobile', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
])
->addImageStyleMapping('responsive_image_test_module.narrow', '1x', [
'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', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
])
->save();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// Refresh the page.
$this->drupalGet($manage_display);
$assert_session->responseContains("Select a responsive image style.");
// Click on the formatter settings button to open the formatter settings
// form.
$field_image_type = $page->findField('fields[field_image][type]');
$field_image_type->setValue('responsive_image');
$page->find('css', '#edit-fields-field-image-settings-edit')->click();
$assert_session->waitForField('fields[field_image][settings_edit_form][settings][responsive_image_style]');
// Assert that the correct fields are present.
$fieldnames = [
'fields[field_image][settings_edit_form][settings][responsive_image_style]',
'fields[field_image][settings_edit_form][settings][image_link]',
];
foreach ($fieldnames as $fieldname) {
$assert_session->fieldExists($fieldname);
}
$page->findField('fields[field_image][settings_edit_form][settings][responsive_image_style]')->setValue('style_one');
$page->findField('fields[field_image][settings_edit_form][settings][image_link]')->setValue('content');
// Save the form to save the settings.
$page->pressButton('Save');
$assert_session->responseContains('Responsive image style: Style One');
$assert_session->responseContains('Linked to content');
$page->find('css', '#edit-fields-field-image-settings-edit')->click();
$assert_session->waitForField('fields[field_image][settings_edit_form][settings][responsive_image_style]');
$page->findField('fields[field_image][settings_edit_form][settings][image_link]')->setValue('file');
// Save the form to save the settings.
$page->pressButton('Save');
$assert_session->responseContains('Responsive image style: Style One');
$assert_session->responseContains('Linked to file');
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Kernel\Migrate\d7;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of responsive image styles.
*
* @group responsive_image
*/
class MigrateResponsiveImageStylesTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['responsive_image', 'breakpoint', 'image'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Ensure the 'picture' module is enabled in the source.
$this->sourceDatabase->update('system')
->condition('name', 'picture')
->fields(['status' => 1])
->execute();
$this->executeMigrations(['d7_image_styles', 'd7_responsive_image_styles']);
}
/**
* Tests the Drupal 7 to Drupal 8 responsive image styles migration.
*/
public function testResponsiveImageStyles(): void {
$expected_image_style_mappings = [
[
'image_mapping_type' => 'image_style',
'image_mapping' => 'custom_image_style_1',
'breakpoint_id' => 'responsive_image.computer',
'multiplier' => 'multiplier_1',
],
[
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '2',
'sizes_image_styles' => [
'custom_image_style_1',
'custom_image_style_2',
],
],
'breakpoint_id' => 'responsive_image.computer',
'multiplier' => 'multiplier_2',
],
[
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '2',
'sizes_image_styles' => [
'custom_image_style_1',
'custom_image_style_2',
],
],
'breakpoint_id' => 'responsive_image.computertwo',
'multiplier' => 'multiplier_2',
],
];
$this->assertSame($expected_image_style_mappings, ResponsiveImageStyle::load('narrow')
->getImageStyleMappings());
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests D7 responsive image styles source plugin.
*
* @covers \Drupal\responsive_image\Plugin\migrate\source\d7\ResponsiveImageStyles
* @group image
*/
class ResponsiveImageStylesTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate_drupal',
'responsive_image',
];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// The source data.
$tests[0]['source_data']['picture_mapping'] = [
[
'label' => 'Narrow',
'machine_name' => 'narrow',
'breakpoint_group' => 'responsive_image',
'mapping' => 'a:2:{s:38:"breakpoints.theme.my_theme_id.computer";a:3:{s:12:"multiplier_1";a:2:{s:12:"mapping_type";s:11:"image_style";s:11:"image_style";s:20:"custom_image_style_1";}s:12:"multiplier_2";a:3:{s:12:"mapping_type";s:5:"sizes";s:5:"sizes";i:2;s:18:"sizes_image_styles";a:2:{i:0;s:20:"custom_image_style_1";i:1;s:20:"custom_image_style_2";}}s:12:"multiplier_3";a:1:{s:12:"mapping_type";s:5:"_none";}}s:42:"breakpoints.theme.my_theme_id.computer_two";a:1:{s:12:"multiplier_2";a:3:{s:12:"mapping_type";s:5:"sizes";s:5:"sizes";i:2;s:18:"sizes_image_styles";a:2:{i:0;s:20:"custom_image_style_1";i:1;s:20:"custom_image_style_2";}}}}',
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'label' => 'Narrow',
'machine_name' => 'narrow',
'breakpoint_group' => 'responsive_image',
'mapping' => [
'breakpoints.theme.my_theme_id.computer' =>
[
'multiplier_1' =>
[
'mapping_type' => 'image_style',
'image_style' => 'custom_image_style_1',
],
'multiplier_2' =>
[
'mapping_type' => 'sizes',
'sizes' => 2,
'sizes_image_styles' =>
[
0 => 'custom_image_style_1',
1 => 'custom_image_style_2',
],
],
'multiplier_3' =>
[
'mapping_type' => '_none',
],
],
'breakpoints.theme.my_theme_id.computer_two' =>
[
'multiplier_2' =>
[
'mapping_type' => 'sizes',
'sizes' => 2,
'sizes_image_styles' =>
[
0 => 'custom_image_style_1',
1 => 'custom_image_style_2',
],
],
],
],
],
];
return $tests;
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Kernel;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
/**
* Tests the integration of responsive image with other components.
*
* @group responsive_image
*/
class ResponsiveImageIntegrationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'responsive_image',
'field',
'image',
'file',
'entity_test',
'breakpoint',
'responsive_image_test_module',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test');
}
/**
* Tests integration with entity view display.
*/
public function testEntityViewDisplayDependency(): void {
// Create a responsive image style.
ResponsiveImageStyle::create([
'id' => 'foo',
'label' => 'Foo',
'breakpoint_group' => 'responsive_image_test_module',
])->save();
// Create an image field to be used with a responsive image formatter.
FieldStorageConfig::create([
'type' => 'image',
'entity_type' => 'entity_test',
'field_name' => 'bar',
])->save();
FieldConfig::create([
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'field_name' => 'bar',
])->save();
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$display->setComponent('bar', [
'type' => 'responsive_image',
'label' => 'hidden',
'settings' => ['responsive_image_style' => 'foo', 'image_link' => ''],
'third_party_settings' => [],
])->save();
// Check that the 'foo' field is on the display.
$this->assertNotNull($display = EntityViewDisplay::load('entity_test.entity_test.default'));
$this->assertNotEmpty($display->getComponent('bar'));
$this->assertArrayNotHasKey('bar', $display->get('hidden'));
// Delete the responsive image style.
ResponsiveImageStyle::load('foo')->delete();
// Check that the view display was not deleted.
$this->assertNotNull($display = EntityViewDisplay::load('entity_test.entity_test.default'));
// Check that the 'foo' field was disabled.
$this->assertNull($display->getComponent('bar'));
$this->assertArrayHasKey('bar', $display->get('hidden'));
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
/**
* Tests validation of responsive_image_style entities.
*
* @group responsive_image
*/
class ResponsiveImageStyleValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['breakpoint', 'image', 'responsive_image'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = ResponsiveImageStyle::create([
'id' => 'test',
'label' => 'Test',
]);
$this->entity->save();
}
}

View File

@@ -0,0 +1,404 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\responsive_image\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\responsive_image\Entity\ResponsiveImageStyle
* @group block
*/
class ResponsiveImageStyleConfigEntityUnitTest extends UnitTestCase {
/**
* The entity type used for testing.
*
* @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $entityType;
/**
* The entity type manager used for testing.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $entityTypeManager;
/**
* The breakpoint manager used for testing.
*
* @var \Drupal\breakpoint\BreakpointManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $breakpointManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityType = $this->createMock('\Drupal\Core\Entity\EntityTypeInterface');
$this->entityType->expects($this->any())
->method('getProvider')
->willReturn('responsive_image');
$this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
$this->entityTypeManager->expects($this->any())
->method('getDefinition')
->with('responsive_image_style')
->willReturn($this->entityType);
$this->breakpointManager = $this->createMock('\Drupal\breakpoint\BreakpointManagerInterface');
$container = new ContainerBuilder();
$container->set('entity_type.manager', $this->entityTypeManager);
$container->set('breakpoint.manager', $this->breakpointManager);
\Drupal::setContainer($container);
}
/**
* @covers ::calculateDependencies
*/
public function testCalculateDependencies(): void {
// Set up image style loading mock.
$styles = [];
foreach (['fallback', 'small', 'medium', 'large'] as $style) {
$mock = $this->createMock('Drupal\Core\Config\Entity\ConfigEntityInterface');
$mock->expects($this->any())
->method('getConfigDependencyName')
->willReturn('image.style.' . $style);
$styles[$style] = $mock;
}
$storage = $this->createMock('\Drupal\Core\Config\Entity\ConfigEntityStorageInterface');
$storage->expects($this->any())
->method('loadMultiple')
->with(array_keys($styles))
->willReturn($styles);
$this->entityTypeManager->expects($this->any())
->method('getStorage')
->with('image_style')
->willReturn($storage);
$entity_type_repository = $this->createMock(EntityTypeRepositoryInterface::class);
$entity_type_repository->expects($this->any())
->method('getEntityTypeFromClass')
->with('Drupal\image\Entity\ImageStyle')
->willReturn('image_style');
$entity = new ResponsiveImageStyle(['breakpoint_group' => 'test_group']);
$entity->setBreakpointGroup('test_group');
$entity->setFallbackImageStyle('fallback');
$entity->addImageStyleMapping('test_breakpoint', '1x', ['image_mapping_type' => 'image_style', 'image_mapping' => 'small']);
$entity->addImageStyleMapping('test_breakpoint', '2x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'medium' => 'medium',
'large' => 'large',
],
],
]);
$this->breakpointManager->expects($this->any())
->method('getGroupProviders')
->with('test_group')
->willReturn(['olivero' => 'theme', 'toolbar' => 'module']);
\Drupal::getContainer()->set('entity_type.repository', $entity_type_repository);
$dependencies = $entity->calculateDependencies()->getDependencies();
$this->assertEquals(['toolbar'], $dependencies['module']);
$this->assertEquals(['olivero'], $dependencies['theme']);
$this->assertEquals(['image.style.fallback', 'image.style.large', 'image.style.medium', 'image.style.small'], $dependencies['config']);
}
/**
* @covers ::addImageStyleMapping
* @covers ::hasImageStyleMappings
*/
public function testHasImageStyleMappings(): void {
$entity = new ResponsiveImageStyle([]);
$this->assertFalse($entity->hasImageStyleMappings());
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => '',
]);
$this->assertFalse($entity->hasImageStyleMappings());
$entity->removeImageStyleMappings();
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [],
],
]);
$this->assertFalse($entity->hasImageStyleMappings());
$entity->removeImageStyleMappings();
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '',
'sizes_image_styles' => [
'large' => 'large',
],
],
]);
$this->assertFalse($entity->hasImageStyleMappings());
$entity->removeImageStyleMappings();
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
]);
$this->assertTrue($entity->hasImageStyleMappings());
$entity->removeImageStyleMappings();
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
],
],
]);
$this->assertTrue($entity->hasImageStyleMappings());
}
/**
* @covers ::addImageStyleMapping
* @covers ::getImageStyleMapping
*/
public function testGetImageStyleMapping(): void {
$entity = new ResponsiveImageStyle(['']);
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
]);
$expected = [
'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(): void {
$entity = new ResponsiveImageStyle(['']);
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
]);
$entity->addImageStyleMapping('test_breakpoint', '2x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
],
],
]);
$entity->addImageStyleMapping('test_breakpoint2', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
]);
$entity->addImageStyleMapping('test_breakpoint2', '2x', [
'image_mapping_type' => 'image_style',
'image_mapping' => '_original image_',
]);
$expected = [
'test_breakpoint' => [
'1x' => [
'breakpoint_id' => 'test_breakpoint',
'multiplier' => '1x',
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
],
'2x' => [
'breakpoint_id' => 'test_breakpoint',
'multiplier' => '2x',
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
],
],
],
],
'test_breakpoint2' => [
'1x' => [
'breakpoint_id' => 'test_breakpoint2',
'multiplier' => '1x',
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
],
'2x' => [
'breakpoint_id' => 'test_breakpoint2',
'multiplier' => '2x',
'image_mapping_type' => 'image_style',
'image_mapping' => '_original image_',
],
],
];
$this->assertEquals($expected, $entity->getKeyedImageStyleMappings());
// Add another mapping to ensure keyed mapping static cache is rebuilt.
$entity->addImageStyleMapping('test_breakpoint2', '2x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'medium',
]);
$expected['test_breakpoint2']['2x'] = [
'breakpoint_id' => 'test_breakpoint2',
'multiplier' => '2x',
'image_mapping_type' => 'image_style',
'image_mapping' => 'medium',
];
$this->assertEquals($expected, $entity->getKeyedImageStyleMappings());
// Overwrite a mapping to ensure keyed mapping static cache is rebuilt.
$entity->addImageStyleMapping('test_breakpoint2', '2x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
]);
$expected['test_breakpoint2']['2x'] = [
'breakpoint_id' => 'test_breakpoint2',
'multiplier' => '2x',
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
];
$this->assertEquals($expected, $entity->getKeyedImageStyleMappings());
}
/**
* @covers ::addImageStyleMapping
* @covers ::getImageStyleMappings
*/
public function testGetImageStyleMappings(): void {
$entity = new ResponsiveImageStyle(['']);
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
]);
$entity->addImageStyleMapping('test_breakpoint', '2x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
],
],
]);
$entity->addImageStyleMapping('test_breakpoint2', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
]);
$expected = [
[
'breakpoint_id' => 'test_breakpoint',
'multiplier' => '1x',
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
],
[
'breakpoint_id' => 'test_breakpoint',
'multiplier' => '2x',
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
],
],
],
[
'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(): void {
$entity = new ResponsiveImageStyle(['']);
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
]);
$entity->addImageStyleMapping('test_breakpoint', '2x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
],
],
]);
$entity->addImageStyleMapping('test_breakpoint2', '1x', [
'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(): void {
$entity = new ResponsiveImageStyle(['breakpoint_group' => 'test_group']);
$entity->addImageStyleMapping('test_breakpoint', '1x', [
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
]);
$entity->addImageStyleMapping('test_breakpoint', '2x', [
'image_mapping_type' => 'sizes',
'image_mapping' => [
'sizes' => '(min-width:700px) 700px, 100vw',
'sizes_image_styles' => [
'large' => 'large',
],
],
]);
$entity->addImageStyleMapping('test_breakpoint2', '1x', [
'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());
}
}