Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

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

View file

@ -0,0 +1,268 @@
<?php
namespace Drupal\responsive_image\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\image\Entity\ImageStyle;
use Drupal\responsive_image\ResponsiveImageStyleInterface;
/**
* Defines the responsive image style entity.
*
* @ConfigEntityType(
* id = "responsive_image_style",
* label = @Translation("Responsive image style"),
* 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"
* },
* 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 = array();
/**
* @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 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 = array(
'breakpoint_id' => $breakpoint_id,
'multiplier' => $multiplier,
) + $image_style_mapping;
$this->keyedImageStyleMappings = NULL;
return $this;
}
}
$this->image_style_mappings[] = array(
'breakpoint_id' => $breakpoint_id,
'multiplier' => $multiplier,
) + $image_style_mapping;
$this->keyedImageStyleMappings = NULL;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasImageStyleMappings() {
$mappings = $this->getKeyedImageStyleMappings();
return !empty($mappings);
}
/**
* {@inheritdoc}
*/
public function getKeyedImageStyleMappings() {
if (!$this->keyedImageStyleMappings) {
$this->keyedImageStyleMappings = array();
foreach ($this->image_style_mappings as $mapping) {
if (!static::isEmptyImageStyleMapping($mapping)) {
$this->keyedImageStyleMappings[$mapping['breakpoint_id']][$mapping['multiplier']] = $mapping;
}
}
}
return $this->keyedImageStyleMappings;
}
/**
* {@inheritdoc}
*/
public function getImageStyleMappings() {
return $this->image_style_mappings;
}
/**
* {@inheritdoc}
*/
public function setBreakpointGroup($breakpoint_group) {
// If the breakpoint group is changed then the image style mappings are
// invalid.
if ($breakpoint_group !== $this->breakpoint_group) {
$this->removeImageStyleMappings();
}
$this->breakpoint_group = $breakpoint_group;
return $this;
}
/**
* {@inheritdoc}
*/
public function getBreakpointGroup() {
return $this->breakpoint_group;
}
/**
* {@inheritdoc}
*/
public function 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 = array();
$this->keyedImageStyleMappings = NULL;
return $this;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
$providers = \Drupal::service('breakpoint.manager')->getGroupProviders($this->breakpoint_group);
foreach ($providers as $provider => $type) {
$this->addDependency($type, $provider);
}
// 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,265 @@
<?php
namespace Drupal\responsive_image\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
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 = @Translation("Responsive image"),
* field_types = {
* "image",
* }
* )
*/
class ResponsiveImageFormatter extends ImageFormatterBase implements ContainerFactoryPluginInterface {
/**
* @var 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.manager')->getStorage('responsive_image_style'),
$container->get('entity.manager')->getStorage('image_style'),
$container->get('link_generator'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'responsive_image_style' => '',
'image_link' => '',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$responsive_image_options = array();
$responsive_image_styles = $this->responsiveImageStyleStorage->loadMultiple();
if ($responsive_image_styles && !empty($responsive_image_styles)) {
foreach ($responsive_image_styles as $machine_name => $responsive_image_style) {
if ($responsive_image_style->hasImageStyleMappings()) {
$responsive_image_options[$machine_name] = $responsive_image_style->label();
}
}
}
$elements['responsive_image_style'] = array(
'#title' => t('Responsive image style'),
'#type' => 'select',
'#default_value' => $this->getSetting('responsive_image_style'),
'#required' => TRUE,
'#options' => $responsive_image_options,
'#description' => array(
'#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'),
),
);
$link_types = array(
'content' => t('Content'),
'file' => t('File'),
);
$elements['image_link'] = array(
'#title' => t('Link image to'),
'#type' => 'select',
'#default_value' => $this->getSetting('image_link'),
'#empty_option' => t('Nothing'),
'#options' => $link_types,
);
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = array();
$responsive_image_style = $this->responsiveImageStyleStorage->load($this->getSetting('responsive_image_style'));
if ($responsive_image_style) {
$summary[] = t('Responsive image style: @responsive_image_style', array('@responsive_image_style' => $responsive_image_style->label()));
$link_types = array(
'content' => t('Linked to content'),
'file' => 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[] = t('Select a responsive image style.');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = array();
$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->urlInfo();
}
}
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 = array();
$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) {
// Link the <picture> element to the original file.
if (isset($link_file)) {
$url = file_url_transform_relative(file_create_url($file->getFileUri()));
}
// 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);
$elements[$delta] = array(
'#theme' => 'responsive_image_formatter',
'#item' => $item,
'#item_attributes' => $item_attributes,
'#responsive_image_style_id' => $responsive_image_style ? $responsive_image_style->id() : '',
'#url' => $url,
'#cache' => array(
'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,292 @@
<?php
namespace Drupal\responsive_image;
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.
*/
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', array('@label' => $this->entity->label()));
$this->entity = $this->entity->createDuplicate();
}
if ($this->operation == 'edit') {
$form['#title'] = $this->t('<em>Edit responsive image style</em> @label', array('@label' => $this->entity->label()));
}
/** @var \Drupal\responsive_image\ResponsiveImageStyleInterface $responsive_image_style */
$responsive_image_style = $this->entity;
$form['label'] = array(
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $responsive_image_style->label(),
'#description' => $this->t("Example: 'Hero image' or 'Author image'."),
'#required' => TRUE,
);
$form['id'] = array(
'#type' => 'machine_name',
'#default_value' => $responsive_image_style->id(),
'#machine_name' => array(
'exists' => '\Drupal\responsive_image\Entity\ResponsiveImageStyle::load',
'source' => array('label'),
),
'#disabled' => (bool) $responsive_image_style->id() && $this->operation != 'duplicate',
);
$image_styles = image_style_options(TRUE);
$image_styles[RESPONSIVE_IMAGE_ORIGINAL_IMAGE] = $this->t('- None (original image) -');
$image_styles[RESPONSIVE_IMAGE_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'] = array(
'#type' => 'select',
'#title' => $this->t('Breakpoint group'),
'#default_value' => $responsive_image_style->getBreakpointGroup() ?: 'responsive_image',
'#options' => $this->breakpointManager->getGroups(),
'#required' => TRUE,
'#description' => $description,
'#ajax' => array(
'callback' => '::breakpointMappingFormAjax',
'wrapper' => 'responsive-image-style-breakpoints-wrapper',
),
);
$form['keyed_styles'] = array(
'#type' => 'container',
'#attributes' => array(
'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] = array(
'#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.', array(':responsive_image_help' => \Drupal::url('help.page', array('name' => 'responsive_image'))));
}
else {
$description = $this->t('Enable the Help module for more information on the sizes attribute.');
}
$form['keyed_styles'][$breakpoint_id][$multiplier]['image_mapping_type'] = array(
'#title' => $this->t('Type'),
'#type' => 'radios',
'#options' => array(
'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' => isset($image_style_mapping['image_mapping_type']) ? $image_style_mapping['image_mapping_type'] : '_none',
'#description' => $description,
);
$form['keyed_styles'][$breakpoint_id][$multiplier]['image_style'] = array(
'#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' => array(
'visible' => array(
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => array('value' => 'image_style'),
),
),
);
$form['keyed_styles'][$breakpoint_id][$multiplier]['sizes'] = array(
'#type' => 'textfield',
'#title' => $this->t('Sizes'),
'#default_value' => isset($image_style_mapping['image_mapping']['sizes']) ? $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' => array(
'visible' => array(
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => array('value' => 'sizes'),
),
'required' => array(
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => array('value' => 'sizes'),
),
),
);
$form['keyed_styles'][$breakpoint_id][$multiplier]['sizes_image_styles'] = array(
'#title' => $this->t('Image styles'),
'#type' => 'checkboxes',
'#options' => array_diff_key($image_styles, array('' => '')),
'#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' => isset($image_style_mapping['image_mapping']['sizes_image_styles']) ? $image_style_mapping['image_mapping']['sizes_image_styles'] : array(),
'#states' => array(
'visible' => array(
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => array('value' => 'sizes'),
),
'required' => array(
':input[name="keyed_styles[' . $breakpoint_id . '][' . $multiplier . '][image_mapping_type]"]' => array('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'] = array(
'#title' => $this->t('Fallback image style'),
'#type' => 'select',
'#default_value' => $responsive_image_style->getFallbackImageStyle(),
'#options' => $image_styles,
'#required' => TRUE,
'#description' => t('Select the smallest image style you expect to appear in this space. The fallback image style should only appear on the site if an error occurs.'),
);
$form['#tree'] = TRUE;
return parent::form($form, $form_state, $responsive_image_style);
}
/**
* 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');
}
// 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 = array(
'image_mapping_type' => 'sizes',
'image_mapping' => array(
'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 = array(
'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.', array('@label' => $responsive_image_style->label()));
drupal_set_message($this->t('Responsive image style %label saved.', array('%label' => $responsive_image_style->label())));
// Redirect to edit form after creating a new responsive image style or
// after selecting another breakpoint group.
if (!$responsive_image_style->hasImageStyleMappings()) {
$form_state->setRedirect(
'entity.responsive_image_style.edit_form',
array('responsive_image_style' => $responsive_image_style->id())
);
}
else {
$form_state->setRedirectUrl($this->entity->urlInfo('collection'));
}
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace Drupal\responsive_image;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a responsive_image mapping entity.
*/
interface ResponsiveImageStyleInterface extends ConfigEntityInterface {
/**
* 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 a 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,44 @@
<?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');
$header['id'] = t('Machine name');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['id'] = $entity->id();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
$operations['duplicate'] = array(
'title' => t('Duplicate'),
'weight' => 15,
'url' => $entity->urlInfo('duplicate-form'),
);
return $operations;
}
}

View file

@ -0,0 +1,143 @@
<?php
namespace Drupal\responsive_image\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Thoroughly test the administrative interface of the Responsive Image module.
*
* @group responsive_image
*/
class ResponsiveImageAdminUITest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('responsive_image', 'responsive_image_test_module');
/**
* Drupal\simpletest\WebTestBase\setUp().
*/
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'administer responsive images',
]));
}
/**
* Test responsive image administration functionality.
*/
public function testResponsiveImageAdmin() {
// We start without any default styles.
$this->drupalGet('admin/config/media/responsive-image-style');
$this->assertText('There is no Responsive image style yet.');
// Add a responsive image style.
$this->drupalGet('admin/config/media/responsive-image-style/add');
// The 'Responsive Image' breakpoint group should be selected by default.
$this->assertFieldByName('breakpoint_group', 'responsive_image');
// Create a new group.
$edit = array(
'label' => 'Style One',
'id' => 'style_one',
'breakpoint_group' => 'responsive_image_test_module',
'fallback_image_style' => 'thumbnail',
);
$this->drupalPostForm('admin/config/media/responsive-image-style/add', $edit, t('Save'));
// Check if the new group is created.
$this->assertResponse(200);
$this->drupalGet('admin/config/media/responsive-image-style');
$this->assertNoText('There is no Responsive image style yet.');
$this->assertText('Style One');
$this->assertText('style_one');
// Edit the group.
$this->drupalGet('admin/config/media/responsive-image-style/style_one');
$this->assertFieldByName('label', 'Style One');
$this->assertFieldByName('breakpoint_group', 'responsive_image_test_module');
$this->assertFieldByName('fallback_image_style', 'thumbnail');
$cases = array(
array('mobile', '1x'),
array('mobile', '2x'),
array('narrow', '1x'),
array('narrow', '2x'),
array('wide', '1x'),
array('wide', '2x'),
);
$image_styles = array_merge(
[RESPONSIVE_IMAGE_EMPTY_IMAGE, RESPONSIVE_IMAGE_ORIGINAL_IMAGE],
array_keys(image_style_options(FALSE))
);
foreach ($cases as $case) {
// Check if the radio buttons are present.
$this->assertFieldByName('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_mapping_type]', '');
// Check if the image style dropdowns are present.
$this->assertFieldByName('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_style]', '');
// Check if the sizes textfields are present.
$this->assertFieldByName('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->assertTrue($this->xpath(
'//select[@name=:name]//option[@value=:style]',
[
':name' => 'keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][image_style]',
':style' => $image_style_name,
]
));
// Check if the image styles checkboxes are present.
$this->assertFieldByName('keyed_styles[responsive_image_test_module.' . $case[0] . '][' . $case[1] . '][sizes_image_styles][' . $image_style_name . ']');
}
}
// Save styles for 1x variant only.
$edit = array(
'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',
'keyed_styles[responsive_image_test_module.narrow][1x][sizes]' => '(min-width: 700px) 700px, 100vw',
'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->drupalPostForm('admin/config/media/responsive-image-style/style_one', $edit, t('Save'));
$this->drupalGet('admin/config/media/responsive-image-style/style_one');
// Check the mapping for multipliers 1x and 2x for the mobile breakpoint.
$this->assertFieldByName('keyed_styles[responsive_image_test_module.mobile][1x][image_style]', 'thumbnail');
$this->assertFieldByName('keyed_styles[responsive_image_test_module.mobile][1x][image_mapping_type]', 'image_style');
$this->assertFieldByName('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->assertFieldByName('keyed_styles[responsive_image_test_module.narrow][1x][image_mapping_type]', 'sizes');
$this->assertFieldByName('keyed_styles[responsive_image_test_module.narrow][1x][sizes]', '(min-width: 700px) 700px, 100vw');
$this->assertFieldChecked('edit-keyed-styles-responsive-image-test-modulenarrow-1x-sizes-image-styles-large');
$this->assertFieldChecked('edit-keyed-styles-responsive-image-test-modulenarrow-1x-sizes-image-styles-medium');
$this->assertNoFieldChecked('edit-keyed-styles-responsive-image-test-modulenarrow-1x-sizes-image-styles-thumbnail');
$this->assertFieldByName('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->assertFieldByName('keyed_styles[responsive_image_test_module.wide][1x][image_style]', 'large');
$this->assertFieldByName('keyed_styles[responsive_image_test_module.wide][1x][image_mapping_type]', 'image_style');
$this->assertFieldByName('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->drupalPostForm(NULL, array(), t('Delete'));
$this->drupalGet('admin/config/media/responsive-image-style');
$this->assertText('There is no Responsive image style yet.');
}
}

View file

@ -0,0 +1,518 @@
<?php
namespace Drupal\responsive_image\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\image\Tests\ImageFieldTestBase;
use Drupal\image\Entity\ImageStyle;
use Drupal\node\Entity\Node;
use Drupal\file\Entity\File;
use Drupal\responsive_image\Plugin\Field\FieldFormatter\ResponsiveImageFormatter;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\user\RoleInterface;
/**
* Tests responsive image display formatter.
*
* @group responsive_image
*/
class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase {
protected $dumpHeaders = TRUE;
/**
* Responsive image style entity instance we test with.
*
* @var \Drupal\responsive_image\Entity\ResponsiveImageStyle
*/
protected $responsiveImgStyle;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('field_ui', 'responsive_image', 'responsive_image_test_module');
/**
* Drupal\simpletest\WebTestBase\setUp().
*/
protected function setUp() {
parent::setUp();
// Create user.
$this->adminUser = $this->drupalCreateUser(array(
'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(array(
'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() {
$this->addTestImageStyleMappings();
$this->doTestResponsiveImageFieldFormatters('public');
}
/**
* Tests responsive image formatters on node display for private files.
*/
public function testResponsiveImageFieldFormattersPrivate() {
$this->addTestImageStyleMappings();
// Remove access content permission from anonymous users.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array('access content' => FALSE));
$this->doTestResponsiveImageFieldFormatters('private');
}
/**
* Test responsive image formatters when image style is empty.
*/
public function testResponsiveImageFieldFormattersEmptyStyle() {
$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', array(
'image_mapping_type' => 'image_style',
'image_mapping' => '',
))
->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array(
'image_mapping_type' => 'sizes',
'image_mapping' => array(
'sizes' => '(min-width: 700px) 700px, 100vw',
'sizes_image_styles' => array(),
),
))
->addImageStyleMapping('responsive_image_test_module.wide', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => '',
))
->save();
}
else {
$this->responsiveImgStyle
// Test the output of an empty image.
->addImageStyleMapping('responsive_image_test_module.mobile', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => RESPONSIVE_IMAGE_EMPTY_IMAGE,
))
// Test the output with a 1.5x multiplier.
->addImageStyleMapping('responsive_image_test_module.mobile', '1.5x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
))
// Test the output of the 'sizes' attribute.
->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array(
'image_mapping_type' => 'sizes',
'image_mapping' => array(
'sizes' => '(min-width: 700px) 700px, 100vw',
'sizes_image_styles' => array(
'large',
'medium',
),
),
))
// Test the normal output of mapping to an image style.
->addImageStyleMapping('responsive_image_test_module.wide', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
))
// Test the output of the original image.
->addImageStyleMapping('responsive_image_test_module.wide', '3x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => RESPONSIVE_IMAGE_ORIGINAL_IMAGE,
))
->save();
}
}
/**
* Test 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.manager')->getStorage('node');
$field_name = Unicode::strtolower($this->randomMachineName());
$this->createImageField($field_name, 'article', array('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->drupalGetTestFiles('image', 39325));
// Create alt text for the image.
$alt = $this->randomMachineName();
$nid = $this->uploadNodeImage($test_image, $field_name, 'article', $alt);
$node_storage->resetCache(array($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 = array(
'#theme' => 'image',
'#uri' => $image_uri,
'#width' => 360,
'#height' => 240,
'#alt' => $alt,
);
$default_output = str_replace("\n", NULL, $renderer->renderRoot($image));
$this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.');
// Test field not being configured. This should not cause a fatal error.
$display_options = array(
'type' => 'responsive_image_test',
'settings' => ResponsiveImageFormatter::defaultSettings(),
);
$display = $this->container->get('entity.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.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 = array(
'type' => 'responsive_image_test',
'settings' => array(
'image_link' => 'file',
'responsive_image_style' => 'style_one',
),
);
$display = entity_get_display('node', 'article', 'default');
$display->setComponent($field_name, $display_options)
->save();
$this->drupalGet('node/' . $nid);
// Use the responsive image formatter linked to file formatter.
$display_options = array(
'type' => 'responsive_image',
'settings' => array(
'image_link' => 'file',
'responsive_image_style' => 'style_one',
),
);
$display = entity_get_display('node', 'article', 'default');
$display->setComponent($field_name, $display_options)
->save();
$default_output = '<a href="' . file_url_transform_relative(file_create_url($image_uri)) . '"><picture';
$this->drupalGet('node/' . $nid);
$cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags');
$this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.');
$this->removeWhiteSpace();
$this->assertRaw($default_output, 'Image linked to file formatter displaying correctly on full node view.');
// Verify that the image can be downloaded.
$this->assertEqual(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($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->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png', 'Content-Type header was sent.');
$this->assertTrue(strstr($this->drupalGetHeader('Cache-Control'), 'private') !== FALSE, 'Cache-Control header was sent.');
// Log out and try to access the file.
$this->drupalLogout();
$this->drupalGet(file_create_url($image_uri));
$this->assertResponse('403', 'Access denied to original image as anonymous user.');
// 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->assertRaw('/styles/medium/');
// Make sure the IE9 workaround is present.
$this->assertRaw('<!--[if IE 9]><video style="display: none;"><![endif]-->');
$this->assertRaw('<!--[if IE 9]></video><![endif]-->');
// Assert the empty image is present.
$this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
$thumbnail_style = ImageStyle::load('thumbnail');
// Assert the output of the 'srcset' attribute (small multipliers first).
$this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . file_url_transform_relative($thumbnail_style->buildUrl($image_uri)) . ' 1.5x');
$this->assertRaw('/styles/medium/');
// Assert the output of the original image.
$this->assertRaw(file_url_transform_relative(file_create_url($image_uri)) . ' 3x');
// Assert the output of the breakpoints.
$this->assertRaw('media="(min-width: 0px)"');
$this->assertRaw('media="(min-width: 560px)"');
// Assert the output of the 'sizes' attribute.
$this->assertRaw('sizes="(min-width: 700px) 700px, 100vw"');
$this->assertPattern('/media="\(min-width: 560px\)".+?sizes="\(min-width: 700px\) 700px, 100vw"/');
// Assert the output of the 'srcset' attribute (small images first).
$medium_style = ImageStyle::load('medium');
$this->assertRaw(file_url_transform_relative($medium_style->buildUrl($image_uri)) . ' 220w, ' . file_url_transform_relative($large_style->buildUrl($image_uri)) . ' 360w');
$this->assertRaw('media="(min-width: 851px)"');
}
$this->assertRaw('/styles/large/');
$cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
$this->assertTrue(in_array('config:responsive_image.styles.style_one', $cache_tags));
if (!$empty_styles) {
$this->assertTrue(in_array('config:image.style.medium', $cache_tags));
$this->assertTrue(in_array('config:image.style.thumbnail', $cache_tags));
$this->assertRaw('type="image/png"');
}
$this->assertTrue(in_array('config:image.style.large', $cache_tags));
// Test the fallback image style.
$image = \Drupal::service('image.factory')->get($image_uri);
$fallback_image = array(
'#theme' => 'image',
'#alt' => $alt,
'#srcset' => array(
array(
'uri' => file_url_transform_relative($large_style->buildUrl($image->getSource())),
),
),
);
// 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($renderer->renderRoot($fallback_image));
$this->assertRaw($default_output, 'Image style large formatter displaying correctly on full node view.');
if ($scheme == 'private') {
// Log out and try to access the file.
$this->drupalLogout();
$this->drupalGet($large_style->buildUrl($image_uri));
$this->assertResponse('403', 'Access denied to image style large as anonymous user.');
$cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags');
$this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.');
}
}
/**
* Tests responsive image formatters on node display linked to the file.
*/
public function testResponsiveImageFieldFormattersLinkToFile() {
$this->addTestImageStyleMappings();
$this->assertResponsiveImageFieldFormattersLink('file');
}
/**
* Tests responsive image formatters on node display linked to the node.
*/
public function testResponsiveImageFieldFormattersLinkToNode() {
$this->addTestImageStyleMappings();
$this->assertResponsiveImageFieldFormattersLink('content');
}
/**
* Tests responsive image formatter on node display with an empty media query.
*/
public function testResponsiveImageFieldFormattersEmptyMediaQuery() {
$this->responsiveImgStyle
// Test the output of an empty media query.
->addImageStyleMapping('responsive_image_test_module.empty', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => RESPONSIVE_IMAGE_EMPTY_IMAGE,
))
// Test the output with a 1.5x multiplier.
->addImageStyleMapping('responsive_image_test_module.mobile', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
))
->save();
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$field_name = Unicode::strtolower($this->randomMachineName());
$this->createImageField($field_name, 'article', array('uri_scheme' => 'public'));
// Create a new node with an image attached.
$test_image = current($this->drupalGetTestFiles('image'));
$nid = $this->uploadNodeImage($test_image, $field_name, 'article', $this->randomMachineName());
$node_storage->resetCache(array($nid));
// Use the responsive image formatter linked to file formatter.
$display_options = array(
'type' => 'responsive_image',
'settings' => array(
'image_link' => '',
'responsive_image_style' => 'style_one',
),
);
$display = entity_get_display('node', 'article', 'default');
$display->setComponent($field_name, $display_options)
->save();
// View the node.
$this->drupalGet('node/' . $nid);
// Assert an empty media attribute is not output.
$this->assertNoPattern('@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->assertPattern('/srcset="' . preg_quote(file_url_transform_relative($thumbnail_style->buildUrl($image_uri)), '/') . ' 1x".+?media="\(min-width: 0px\)"/');
}
/**
* Tests responsive image formatter on node display with one source.
*/
public function testResponsiveImageFieldFormattersOneSource() {
$this->responsiveImgStyle
// Test the output of an empty media query.
->addImageStyleMapping('responsive_image_test_module.empty', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'medium',
))
->addImageStyleMapping('responsive_image_test_module.empty', '2x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
))
->save();
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$field_name = Unicode::strtolower($this->randomMachineName());
$this->createImageField($field_name, 'article', array('uri_scheme' => 'public'));
// Create a new node with an image attached.
$test_image = current($this->drupalGetTestFiles('image'));
$nid = $this->uploadNodeImage($test_image, $field_name, 'article', $this->randomMachineName());
$node_storage->resetCache(array($nid));
// Use the responsive image formatter linked to file formatter.
$display_options = array(
'type' => 'responsive_image',
'settings' => array(
'image_link' => '',
'responsive_image_style' => 'style_one',
),
);
$display = entity_get_display('node', 'article', 'default');
$display->setComponent($field_name, $display_options)
->save();
// View the node.
$this->drupalGet('node/' . $nid);
// Assert the media attribute is present if it has a value.
$large_style = ImageStyle::load('large');
$medium_style = ImageStyle::load('medium');
$node = $node_storage->load($nid);
$image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
$this->assertRaw('<img srcset="' . file_url_transform_relative($medium_style->buildUrl($image_uri)) . ' 1x, ' . file_url_transform_relative($large_style->buildUrl($image_uri)) . ' 2x"');
}
/**
* 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($link_type) {
$field_name = Unicode::strtolower($this->randomMachineName());
$field_settings = array('alt_field_required' => 0);
$this->createImageField($field_name, 'article', array('uri_scheme' => 'public'), $field_settings);
// Create a new node with an image attached.
$test_image = current($this->drupalGetTestFiles('image'));
// Test the image linked to file formatter.
$display_options = array(
'type' => 'responsive_image',
'settings' => array(
'image_link' => $link_type,
'responsive_image_style' => 'style_one',
),
);
entity_get_display('node', 'article', 'default')
->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->assertPattern('/picture/');
$nid = $this->uploadNodeImage($test_image, $field_name, 'article');
$this->container->get('entity.manager')->getStorage('node')->resetCache(array($nid));
$node = Node::load($nid);
// Use the responsive image formatter linked to file formatter.
$display_options = array(
'type' => 'responsive_image',
'settings' => array(
'image_link' => $link_type,
'responsive_image_style' => 'style_one',
),
);
entity_get_display('node', 'article', 'default')
->setComponent($field_name, $display_options)
->save();
// Create a derivative so at least one MIME type will be known.
$large_style = ImageStyle::load('large');
$image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
$large_style->createDerivative($image_uri, $large_style->buildUri($image_uri));
// Output should contain all image styles and all breakpoints.
$this->drupalGet('node/' . $nid);
$this->removeWhiteSpace();
switch ($link_type) {
case 'file':
// Make sure the link to the file is present.
$this->assertPattern('/<a(.*?)href="' . preg_quote(file_url_transform_relative(file_create_url($image_uri)), '/') . '"(.*?)><picture/');
break;
case 'content':
// Make sure the link to the node is present.
$this->assertPattern('/<a(.*?)href="' . preg_quote($node->url(), '/') . '"(.*?)><picture/');
break;
}
}
}

View file

@ -0,0 +1,128 @@
<?php
namespace Drupal\responsive_image\Tests;
use Drupal\field_ui\Tests\FieldUiTestTrait;
use Drupal\simpletest\WebTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
/**
* Tests the "Responsive Image" formatter settings form.
*
* @group responsive_image
*/
class ResponsiveImageFieldUiTest extends WebTestBase {
use FieldUiTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = array('node', 'field_ui', 'image', 'responsive_image', 'responsive_image_test_module', 'block');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
// Create a test user.
$admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer node fields', 'administer node form display', 'administer node display', 'bypass node access'));
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = strtolower($this->randomMachineName(8)) . '_test';
$type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name));
$this->type = $type->id();
}
/**
* Tests formatter settings.
*/
function testResponsiveImageFormatterUI() {
$manage_fields = 'admin/structure/types/manage/' . $this->type;
$manage_display = $manage_fields . '/display';
// Create a field, and a node with some data for the field.
$this->fieldUIAddNewField($manage_fields, 'image', 'Image field', 'image');
// Display the "Manage display".
$this->drupalGet($manage_display);
// Change the formatter and check that the summary is updated.
$edit = array('fields[field_image][type]' => 'responsive_image', 'refresh_rows' => 'field_image');
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$this->assertText("Select a responsive image style.", 'The expected summary is displayed.');
// Submit the form.
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertText("Select a responsive image style.", 'The expected summary is displayed.');
// Create responsive image styles.
$responsive_image_style = ResponsiveImageStyle::create(array(
'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', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'thumbnail',
))
->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'medium'
))
// Test the normal output of mapping to an image style.
->addImageStyleMapping('responsive_image_test_module.wide', '1x', array(
'image_mapping_type' => 'image_style',
'image_mapping' => 'large',
))
->save();
\Drupal::entityManager()->clearCachedFieldDefinitions();
// Refresh the page.
$this->drupalGet($manage_display);
$this->assertText("Select a responsive image style.", 'The expected summary is displayed.');
// Click on the formatter settings button to open the formatter settings
// form.
$this->drupalPostAjaxForm(NULL, array(), "field_image_settings_edit");
// Assert that the correct fields are present.
$fieldnames = array(
'fields[field_image][settings_edit_form][settings][responsive_image_style]',
'fields[field_image][settings_edit_form][settings][image_link]',
);
foreach ($fieldnames as $fieldname) {
$this->assertField($fieldname);
}
$edit = array(
'fields[field_image][settings_edit_form][settings][responsive_image_style]' => 'style_one',
'fields[field_image][settings_edit_form][settings][image_link]' => 'content',
);
$this->drupalPostAjaxForm(NULL, $edit, "field_image_plugin_settings_update");
// Save the form to save the settings.
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertText('Responsive image style: Style One');
$this->assertText('Linked to content');
// Click on the formatter settings button to open the formatter settings
// form.
$this->drupalPostAjaxForm(NULL, array(), "field_image_settings_edit");
$edit = array(
'fields[field_image][settings_edit_form][settings][responsive_image_style]' => 'style_one',
'fields[field_image][settings_edit_form][settings][image_link]' => 'file',
);
$this->drupalPostAjaxForm(NULL, $edit, "field_image_plugin_settings_update");
// Save the form to save the third party settings.
$this->drupalPostForm(NULL, array(), t('Save'));
$this->assertText('Responsive image style: Style One');
$this->assertText('Linked to file');
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\responsive_image\Tests\Update;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Serialization\Yaml;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests responsive image module updates.
*
* @group responsive_image
*/
class ResponsiveImageUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8-rc1.bare.standard.php.gz',
];
}
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
/** @var \Drupal\Core\State\StateInterface $state */
$state = $this->container->get('state');
// Enable responsive_image module without using the module installer to
// avoid installation of configuration shipped in module.
$system_module_files = $state->get('system.module.files', []);
$system_module_files += ['responsive_image' => 'core/modules/responsive_image/responsive_image.info.yml'];
$state->set('system.module.files', $system_module_files);
$this->config('core.extension')->set('module.responsive_image', 0)->save();
$this->container->get('module_handler')->addModule('responsive_image', 'core/modules/responsive_image');
}
/**
* Tests post-update responsive_image_post_update_dependency().
*
* @see responsive_image_post_update_dependency()
*/
public function testPostUpdateDependency() {
// Installing the 'wide' responsive image style.
$wide_image_style = Yaml::decode(file_get_contents(__DIR__ . '/../../../../../profiles/standard/config/optional/responsive_image.styles.wide.yml'));
$this->config('responsive_image.styles.wide')->setData($wide_image_style)->save(TRUE);
// Change 'field_image' formatter to a responsive image formatter.
$options = [
'type' => 'responsive_image',
'label' => 'hidden',
'settings' => ['responsive_image_style' => 'wide', 'image_link' => ''],
'third_party_settings' => [],
];
$display = $this->config('core.entity_view_display.node.article.default');
$display->set('content.field_image', $options)->save(TRUE);
// Check that there's no dependency to 'responsive_image.styles.wide'.
$dependencies = $display->get('dependencies.config') ?: [];
$this->assertFalse(in_array('responsive_image.styles.wide', $dependencies));
// Run updates.
$this->runUpdates();
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */
$view_display = EntityViewDisplay::load('node.article.default');
$dependencies = $view_display->getDependencies() + ['config' => []];
// Check that post-update added a 'responsive_image.styles.wide' dependency.
$this->assertTrue(in_array('responsive_image.styles.wide', $dependencies['config']));
}
}