- π§πͺBelgium nightlife2008
I'm currently doing a D10 project using Bynder and I came to the same requirement of having responsive images with different aspect ratio's for different screensizes, so I decided to create a (rough) formatter to accomodate this somehow by using different DAT queries for different breakpoints / multipliers. This formatter outputs a picture element just like core's Responsive Image module.
I reused some bits of the Responsive Image UI and the original formatter:
Formatter:
<?php namespace Drupal\MODULE\Plugin\Field\FieldFormatter; use Drupal\bynder\Plugin\Field\FieldFormatter\BynderFormatterBase; use Drupal\bynder\Plugin\media\Source\Bynder; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Access\AccessResult; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of the 'Bynder' formatter. * * @FieldFormatter( * id = "bynder_responsive_image", * label = @Translation("Bynder (Image, Responsive picture)"), * field_types = {"string", "string_long", "entity_reference"} * ) */ class BynderFormatterResponsiveImage extends BynderFormatterBase { /** * The entity repository service. * * @var \Drupal\Core\Entity\EntityRepositoryInterface */ protected $entityRepository; /** * The Breakpoint manager service. * * @var \Drupal\breakpoint\BreakpointManagerInterface */ protected $breakpointManager; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); $instance->entityRepository = $container->get('entity.repository'); $instance->breakpointManager = $container->get('breakpoint.manager'); return $instance; } /** * {@inheritdoc} */ public static function defaultSettings() { return [ 'alt_field' => '', 'title_field' => '', 'dat_query' => '', 'breakpoint_group' => 'responsive_image', 'dat_query_responsive' => [], ] + parent::defaultSettings(); } /** * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { $form = parent::settingsForm($form, $form_state); $field_candidates = $this->getAttributesFieldsCandidates(); $form['alt_field'] = [ '#type' => 'select', '#options' => $field_candidates, '#title' => $this->t('Alt attribute field'), '#description' => $this->t('Select the name of the field that should be used for the "alt" attribute of the image.'), '#default_value' => $this->getSetting('alt_field'), '#empty_value' => '', ]; $form['title_field'] = [ '#type' => 'select', '#options' => $field_candidates, '#title' => $this->t('Title attribute field'), '#description' => $this->t('Select the name of the field that should be used for the "title" attribute of the image.'), '#default_value' => $this->getSetting('title_field'), '#empty_value' => '', ]; $dat_documentation = 'https://support.bynder.com/hc/en-us/articles/360018559260-Dynamic-Asset-Transformations-DAT-'; $form['dat_query'] = [ '#type' => 'textfield', '#title' => $this->t('DAT query field'), '#description' => $this->t('Attributes that should be applied to the images. See <a href=":dat_help">here</a> for explanation on possible values. Should start right after the "?", e.g. "io=transform:fill,width:100,height:200" If the following Responsive image fields are filled, this field defines the fallback image if the responsive settings are broken.', [ ':dat_help' => $dat_documentation, ]), '#default_value' => $this->getSetting('dat_query'), '#states' => [ 'visible' => [ ':input.bynder-derivative' => ['value' => 'DAT'], ], 'required' => [ ':input.bynder-derivative' => ['value' => 'DAT'], ], ], ]; $breakpoint_group = $this->getSettingFromFormState($form_state, 'breakpoint_group'); $form['breakpoint_group'] = [ '#type' => 'select', '#title' => $this->t('Breakpoint group'), '#default_value' => $breakpoint_group ?? 'responsive_image', '#options' => $this->breakpointManager->getGroups(), '#required' => TRUE, '#ajax' => [ 'callback' => [get_class(), 'breakpointMappingFormAjax'], 'wrapper' => 'ajax-responsive-query-settings', 'method' => 'replace', ], ]; $form['dat_query_responsive'] = [ '#type' => 'container', '#weight' => 100, '#tree' => TRUE, '#attributes' => [ 'id' => 'ajax-responsive-query-settings', ], '#states' => [ 'visible' => [ ':input.bynder-derivative' => ['value' => 'DAT'], ], ], ]; if (empty($breakpoint_group)) { return $form; } $breakpoints = $this->breakpointManager->getBreakpointsByGroup($breakpoint_group); $dat_query_mapping = $this->getSetting('dat_query_responsive') ?? []; foreach ($breakpoints as $breakpoint_id => $breakpoint) { foreach ($breakpoint->getMultipliers() as $multiplier) { $sanitized_breakpoint_id = str_replace(['-', '.'], '_', $breakpoint_id); $sanitized_multiplier = str_replace(['-', '.'], '_', $multiplier); $item_mapping = $dat_query_mapping[$sanitized_breakpoint_id][$sanitized_multiplier] ?? []; $item_mapping_dat_query = isset($item_mapping['dat_query']) && is_string($item_mapping['dat_query']) ? $item_mapping['dat_query'] : NULL; $label = $multiplier . ' ' . $breakpoint->getLabel() . ' [' . $breakpoint->getMediaQuery() . ']'; $form['dat_query_responsive'][$sanitized_breakpoint_id][$sanitized_multiplier] = [ '#type' => 'details', '#title' => $label, '#open' => !empty($item_mapping_dat_query), ]; $form['dat_query_responsive'][$sanitized_breakpoint_id][$sanitized_multiplier]['dat_query'] = [ '#type' => 'textfield', '#title' => $this->t('Query'), '#default_value' => $item_mapping_dat_query, ]; } } return $form; } /** * Ajax callback to refresh the breakpoint mapping form. */ public static function breakpointMappingFormAjax($form, FormStateInterface $form_state) { $triggeringElement = $form_state->getTriggeringElement(); // Dynamically return the dependent ajax for elements based on the // triggering element. This shouldn't be done statically because // settings forms may be different, e.g. for layout builder, core, ... if (!empty($triggeringElement['#array_parents'])) { $subformKeys = $triggeringElement['#array_parents']; // Remove the triggering element itself: array_pop($subformKeys); $subformKeys[] = 'dat_query_responsive'; // Return the subform: return NestedArray::getValue($form, $subformKeys); } } /** * Get a setting value from form state or from our settings. * * @param \Drupal\Core\Form\FormStateInterface $form_state * The FormState object. * @param $setting * The setting key. * * @return mixed|null * The value. */ protected function getSettingFromFormState(FormStateInterface $form_state, $setting) { $field_name = $this->fieldDefinition->getName(); if ($form_state->hasValue(['fields', $field_name, 'settings_edit_form', 'settings', $setting])) { return $form_state->getValue(['fields', $field_name, 'settings_edit_form', 'settings', $setting]); } return $this->getSetting($setting); } /** * {@inheritdoc} */ public function settingsSummary() { $summary = parent::settingsSummary(); $settings = $this->getSettings(); if (!empty($settings['dat_query_responsive'])) { $summary[] = $this->t('DAT configuration: Responsive, fallback: @dat', ['@dat' => $settings['dat_query']]); } else { $summary[] = $this->t('Fallback DAT configuration: @dat', ['@dat' => $settings['dat_query']]); } $field_candidates = $this->getAttributesFieldsCandidates(); if (empty($settings['title_field'])) { $summary[] = $this->t('Title attribute not displayed (not recommended).'); } else { $summary[] = $this->t('Title attribute field: @field', ['@field' => $field_candidates[$settings['title_field']]]); } if (empty($settings['alt_field'])) { $summary[] = $this->t('Alt attribute not displayed (not recommended).'); } else { $summary[] = $this->t('Alt attribute field: @field', ['@field' => $field_candidates[$settings['alt_field']]]); } return $summary; } /** * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items, $langcode) { $settings = $this->getSettings(); $elements = []; $is_entityreference = $this->fieldDefinition->getType() == 'entity_reference'; foreach ($items as $delta => $item) { /** @var \Drupal\media\MediaInterface $media_entity */ if ($media_entity = $is_entityreference ? $item->entity : $items->getEntity()) { /** @var \Drupal\media\MediaInterface $media_entity */ $media_entity = $this->entityRepository->getTranslationFromContext($media_entity, $langcode); /** @var \Drupal\media\MediaSourceInterface $source_plugin */ $source_plugin = $media_entity->getSource(); if ($source_plugin instanceof Bynder && ($thumbnails = $source_plugin->getMetadata($media_entity, 'thumbnail_urls'))) { $thumbnail_uri = NULL; if (isset($thumbnails['transformBaseUrl']) && !empty($settings['dat_query'])) { $thumbnail_uri = $thumbnails['transformBaseUrl'] . '?' . $this->getSetting('dat_query'); } $attributes = []; $attributes['loading'] = 'lazy'; if ($settings['title_field'] && $media_entity->hasField($settings['title_field']) && !$media_entity->get($settings['title_field'])->isEmpty()) { $attributes['title'] = $media_entity->get($settings['title_field'])->value; } if ($settings['alt_field'] && $media_entity->hasField($settings['alt_field']) && !$media_entity->get($settings['alt_field'])->isEmpty()) { $attributes['alt'] = $media_entity->get($settings['alt_field'])->value; } $elements[$delta] = [ '#theme' => 'bynder_responsive_image', '#uri' => $thumbnail_uri, '#attributes' => $attributes, '#breakpoint_group' => $this->getSetting('breakpoint_group'), '#dat_base_url' => $thumbnails['transformBaseUrl'], '#dat_query_responsive' => $this->getSetting('dat_query_responsive'), ]; $this->renderer->addCacheableDependency($elements[$delta], $item); } } } return $elements; } }
Theme hook:
/** * Implements hook_theme(). */ function MODULE_theme($existing, $type, $theme, $path) { return [ 'bynder_responsive_image' => [ 'variables' => [ 'uri' => NULL, 'attributes' => [], 'breakpoint_group' => [], 'dat_base_url' => '', 'dat_query_responsive' => [], 'height' => NULL, 'width' => NULL, ], ], ]; }
A preprocess function for our template:
/** * Implements hook_preprocess_hook(). */ function template_preprocess_bynder_responsive_image(array &$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']); } // 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. $dat_query_mapping = $variables['dat_query_responsive']; $breakpoint_group = $variables['breakpoint_group']; $breakpoints = array_reverse(\Drupal::service('breakpoint.manager')->getBreakpointsByGroup($breakpoint_group)); foreach ($breakpoints as $breakpoint_id => $breakpoint) { $sanitized_breakpoint_id = str_replace(['-', '.'], '_', $breakpoint_id); $multipliers = $dat_query_mapping[$sanitized_breakpoint_id]; if (isset($dat_query_mapping[$sanitized_breakpoint_id])) { if ($sources = _MODULE_bynder_responsive_image_build_source_attributes($variables, $breakpoint, $multipliers)) { $variables['sources'][] = $sources; } } } 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' => $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' => $variables['uri'], '#attributes' => [], ]; } 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']; } }
The mentioned helper function:
function _MODULE_bynder_responsive_image_build_source_attributes(array $variables, BreakpointInterface $breakpoint, array $multipliers) { $srcset = []; $dat_base_url = $variables['dat_base_url']; // 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 => $multiplier_settings) { if (!empty($multiplier_settings['dat_query'])) { $srcset[intval(mb_substr($multiplier, 0, -1) * 100)] = str_replace('io=', $dat_base_url . '?io=', $multiplier_settings['dat_query']); } } if (empty($srcset)) { return NULL; } // 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); } return $source_attributes; }
Finally the Twig template in our module's "templates" dir:
{# /** * @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 %}
I hope I can help someone looking for the same functionality.
The settings UI can be seen in the attached screenshots.
- πΊπΈUnited States safetypin Memphis, Tennessee
Has anyone considered adding support for the Image Replace Effect module? It can integrate already with an image source media entity, so I would imagine it might not be too hard to extend support to Bynder media entities. I'll start to look into it, and if I develop anything useful I'll start a new thread. Not sure if it should be a patch to this module, a patch to the Replace Effect module, or a whole 'nother module all together.