Commit f8eb47e9 authored by catch's avatar catch
Browse files

Issue #3192234 by heddn, pivica, Qusai Taha, yogeshmpawar, bingolitte, Phil...

Issue #3192234 by heddn, pivica, Qusai Taha, yogeshmpawar, bingolitte, Phil Wolstenholme, smustgrave, Berdir, devkinetic, maximpodorov, ranjith_kumar_k_u, nikitagupta, ankithashetty, vsujeetkumar, pooja saraah, timohuisman, glynster, Luke.Leber, Ahmad Abbad, flyke, chetanbharambe, Graber, zcht, sasanikolic, John Pitcairn, catch, Rar9, Anybody, Ambient.Impact, alexpott, manarak, Fabianx, longwave, benmorss, Martijn de Wit, Wim Leers, larowlan: Apply width and height attributes to allow responsive image tag use loading="lazy"
parent 76c529a6
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -67,3 +67,10 @@ field.formatter.settings.responsive_image:
    image_link:
      type: string
      label: 'Link image to'
    image_loading:
      type: mapping
      label: 'Image loading settings'
      mapping:
        attribute:
          type: string
          label: 'Loading attribute'
+47 −4
Original line number Diff line number Diff line
@@ -5,12 +5,14 @@
 * 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;

@@ -31,11 +33,11 @@ function responsive_image_help($route_name, RouteMatchInterface $route_match) {
      $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> 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 .= '<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 option allows you to provide more options to browsers as to which image file it can display, even when using multiple breakpoints for art direction. Breakpoints are defined in the configuration files of the theme.') . '</dd>';
      $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('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 .= '<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>';
@@ -186,7 +188,20 @@ function template_preprocess_responsive_image(&$variables) {
    $variables['img_element'] = [
      '#theme' => 'image',
      '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
      '#attributes' => [],
    ];

    // We don't set dimensions for fallback image if rendered in picture tag.
    // In Firefox, it results in sizing the entire picture element to the size
    // of the fallback image, instead of the size on the source element.
    $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'];
  }
  else {
    $variables['output_image_tag'] = FALSE;
@@ -195,6 +210,7 @@ function template_preprocess_responsive_image(&$variables) {
    $variables['img_element'] = [
      '#theme' => 'image',
      '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
      '#attributes' => [],
    ];
  }

@@ -362,7 +378,9 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI
  $sizes = [];
  $srcset = [];
  $derivative_mime_types = [];
  foreach ($multipliers as $multiplier => $image_style_mapping) {
  // 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':
@@ -400,6 +418,7 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI
        // 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;
    }
  }
@@ -418,6 +437,21 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI
  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;
}

@@ -508,3 +542,12 @@ function responsive_image_library_info_alter(array &$libraries, $module) {
    $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);
}
+13 −1
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@
 */

use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\responsive_image\ResponsiveImageConfigUpdater;
use Drupal\responsive_image\ResponsiveImageStyleInterface;

@@ -22,10 +23,21 @@ function responsive_image_removed_post_updates() {
 * 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);
  assert($responsive_image_config_updater instanceof ResponsiveImageConfigUpdater);
  $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);
  });
}
+37 −1
Original line number Diff line number Diff line
@@ -115,6 +115,9 @@ public static function defaultSettings() {
    return [
      'responsive_image_style' => '',
      'image_link' => '',
      'image_loading' => [
        'attribute' => 'lazy',
      ],
    ] + parent::defaultSettings();
  }

@@ -122,6 +125,8 @@ public static function 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');
@@ -145,6 +150,29 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
      ],
    ];

    $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'),
@@ -183,7 +211,12 @@ public function settingsSummary() {
      $summary[] = $this->t('Select a responsive image style.');
    }

    return $summary;
    $image_loading = $this->getSetting('image_loading');
    $summary[] = $this->t('Loading attribute: @attribute', [
      '@attribute' => $image_loading['attribute'],
    ]);

    return array_merge($summary, parent::settingsSummary());
  }

  /**
@@ -236,6 +269,9 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
      $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,
+34 −0
Original line number Diff line number Diff line
@@ -2,6 +2,8 @@

namespace Drupal\responsive_image;

use Drupal\Core\Entity\Display\EntityViewDisplayInterface;

/**
 * Provides a BC layer for modules providing old configurations.
 *
@@ -68,4 +70,36 @@ public function orderMultipliersNumerically(ResponsiveImageStyleInterface $respo
    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 to accommodate the changes described at https://www.drupal.org/node/3279032.', $view_display->id()), E_USER_DEPRECATED);
    }

    return $changed;
  }

}
Loading