diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
index 5ebad71..af2c572 100644
--- a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
@@ -9,6 +9,7 @@
*/
(function (CKEDITOR) {
+
"use strict";
CKEDITOR.plugins.add('drupalimagecaption', {
diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index c283908..ce718a9 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -16,7 +16,7 @@
use Drupal\filter\Plugin\FilterBase;
/**
- * Provides a filter to display image captions and align images.
+ * Provides a filter to caption and align images (as well as other elements).
*
* @Filter(
* id = "filter_caption",
diff --git a/core/modules/image/css/image.admin.css b/core/modules/image/css/image.admin.css
index 3e1f901..b398bcd 100644
--- a/core/modules/image/css/image.admin.css
+++ b/core/modules/image/css/image.admin.css
@@ -16,6 +16,7 @@
.image-style-preview .preview-image {
margin: auto;
position: relative;
+ display: block;
}
.image-style-preview .preview-image .width {
border: 1px solid #666;
diff --git a/core/modules/image/image.module b/core/modules/image/image.module
index 86c878f..53c5197 100644
--- a/core/modules/image/image.module
+++ b/core/modules/image/image.module
@@ -507,3 +507,104 @@ function image_field_instance_config_delete(FieldInstanceConfigInterface $field_
\Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field_instance->uuid());
}
}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for EditorImageDialog.
+ *
+ * Alters the image dialog form for text editor, to allow the user to select an
+ * image style.
+ *
+ * @see \Drupal\editor\Form\EditorImageDialog::buildForm()
+ */
+function image_form_editor_image_dialog_alter(&$form, $form_state) {
+ $filter_format = $form_state['build_info']['args'][0];
+ $filters = $filter_format->filters()->getAll();
+
+ $image_element = $form_state['image_element'];
+
+ // When image style functionality is available, disallow the user from
+ // specifying the dimensions manually, only allow image styles to be picked.
+ if (isset($filters['filter_imagestyle']) && $filters['filter_imagestyle']->status) {
+ // Hide the default width/height form items.
+ $form['dimensions']['#access'] = FALSE;
+
+ $image_options = image_style_options(FALSE);
+ $image_options_keys = array_keys($image_options);
+ $form['image_style'] = array(
+ '#type' => 'item',
+ '#field_prefix' => '
',
+ '#field_suffix' => '
',
+ );
+ $form['image_style']['selection'] = array(
+ '#title' => t('Image style'),
+ '#type' => 'select',
+ '#default_value' => isset($image_element['data-image-style']) ? $image_element['data-image-style'] : $image_options_keys[0],
+ '#options' => $image_options,
+ '#required' => TRUE,
+ '#wrapper_attributes' => array('class' => array('container-inline')),
+ '#attributes' => array('class' => array('container-inline')),
+ '#parents' => array('attributes', 'data-image-style'),
+ );
+ $form['image_style']['preview_toggle'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show preview'),
+ );
+ $image_styles = entity_load_multiple('image_style');
+ foreach ($image_styles as $id => $image_style) {
+ $preview_arguments = array(
+ '#theme' => 'image_style_preview',
+ '#style' => $image_style,
+ );
+ $form['image_style']['preview_' . $id] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Preview of %image-style image style', array('%image-style' => $image_style->label())),
+ '#markup' => drupal_render($preview_arguments),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="image_style[preview_toggle]"]' => array('checked' => TRUE),
+ ':input[name="attributes[data-image-style]"]' => array('value' => $id),
+ ),
+ ),
+ );
+ }
+ $form['#attached']['css'][drupal_get_path('module', 'image') . '/css/image.admin.css'] = array();
+
+ $form['actions']['save_modal']['#validate'][] = 'image_form_editor_image_dialog_validate';
+ }
+}
+
+/**
+ * Form validation handler for EditorImageDialog.
+ *
+ * Ensures the image shown in the text editor matches the chosen image style.
+ *
+ * @see \Drupal\editor\Form\EditorImageDialog::buildForm()
+ * @see \Drupal\editor\Form\EditorImageDialog::validateForm()
+ * @see image_form_editor_image_dialog_alter()
+ */
+function image_form_editor_image_dialog_validate(array &$form, array &$form_state) {
+ $attributes = &$form_state['values']['attributes'];
+ if (!empty($form_state['values']['fid'][0])) {
+ $image_style = entity_load('image_style', $attributes['data-image-style']);
+ $file = file_load($form_state['values']['fid'][0]);
+ $uri = $file->getFileUri();
+
+ // Set the 'src' attribute to the image style URL. FilterImageStyle will
+ // look at the 'data-editor-file-uuid' attribute, not the 'src' attribute to
+ // render the appropriate output.
+ $attributes['src'] = $image_style->buildUrl($uri);
+
+ // Set the 'width' and 'height' attributes to the image style's transformed
+ // dimensions.
+ $image = \Drupal::service('image.factory')->get($uri);
+ if ($image->isSupported()) {
+ $dimensions = array(
+ 'width' => $image->getWidth(),
+ 'height' => $image->getHeight()
+ );
+ $image_style->transformDimensions($dimensions);
+ $attributes['width'] = $dimensions['width'];
+ $attributes['height'] = $dimensions['height'];
+ }
+ }
+}
diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module
index 0c93cb9..4ac1260 100644
--- a/core/modules/responsive_image/responsive_image.module
+++ b/core/modules/responsive_image/responsive_image.module
@@ -9,6 +9,7 @@
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Routing\RouteMatchInterface;
use \Drupal\Core\Template\Attribute;
+use Drupal\file\Entity\File;
/**
* The machine name for the empty image breakpoint image style option.
@@ -340,3 +341,164 @@ function _responsive_image_image_style_url($style_name, $path) {
return entity_load('image_style', $style_name)->buildUrl($path);
}
+/**
+ * Implements hook_form_FORM_ID_alter() for EditorImageDialog.
+ *
+ * Alters the image dialog form for text editor, to allow the user to select a
+ * picture mapping.
+ *
+ * @see \Drupal\editor\Form\EditorImageDialog::buildForm()
+ */
+function responsive_image_form_editor_image_dialog_alter(&$form, $form_state) {
+
+ $filter_format = $form_state['build_info']['args'][0];
+ $filters = $filter_format->filters()->getAll();
+
+ $image_element = $form_state['image_element'];
+
+ // When responsive image functionality is available, disallow the user from
+ // specifying the dimensions manually, and from selecting an image style, only
+ // allowing a picture mapping to be selected.
+ if (isset($filters['filter_picturemapping']) && $filters['filter_picturemapping']->status) {
+
+
+ // Hide the default width/height form items.
+ $form['dimensions']['#access'] = FALSE;
+
+ // Remove the image style selection, if it exists; it does not make sense to
+ // use FilterImageStyle when already using FilterPictureMapping!
+ if (isset($form['image_style'])) {
+ unset($form['image_style']);
+ // Remove its #validate callback as well.
+ $validators = &$form['actions']['save_modal']['#validate'];
+ $index = array_search('image_form_editor_image_dialog_validate', $validators);
+ if ($index !== FALSE) {
+ unset($validators[$index]);
+ }
+ }
+
+ $form['picture_mapping'] = array(
+ '#type' => 'item',
+ );
+ $picture_options = array();
+ $picture_mappings = entity_load_multiple('responsive_image_mapping');
+ if ($picture_mappings && !empty($picture_mappings)) {
+ foreach ($picture_mappings as $machine_name => $picture_mapping) {
+ if ($picture_mapping->hasMappings()) {
+ $picture_options[$machine_name] = $picture_mapping->label();
+ }
+ }
+ }
+ $picture_options_keys = array_keys($picture_options);
+ $form['picture_mapping']['selection'] = array(
+ '#title' => t('Image style'),
+ '#type' => 'select',
+ '#default_value' => isset($image_element['data-picture-mapping']) ? $image_element['data-picture-mapping'] : $picture_options_keys[0],
+ '#options' => $picture_options,
+ '#required' => TRUE,
+ '#wrapper_attributes' => array('class' => array('container-inline')),
+ '#attributes' => array('class' => array('container-inline')),
+ '#parents' => array('attributes', 'data-picture-mapping'),
+ );
+ $form['picture_mapping']['preview_toggle'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show preview'),
+ );
+ foreach ($picture_mappings as $machine_name => $picture_mapping) {
+ if ($picture_mapping->hasMappings()) {
+ $form['picture_mapping']['preview_' . $machine_name] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Preview of %picture-mapping picture mapping', array('%picture-mapping' => $picture_mapping->label())),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="picture_mapping[preview_toggle]"]' => array('checked' => TRUE),
+ ':input[name="attributes[data-picture-mapping]"]' => array('value' => $machine_name),
+ ),
+ ),
+ );
+
+ // Generate breakpoint preview selector.
+ $breakpoint_preview_options = array_combine(array_keys($picture_mapping->getMappings()), $picture_mapping->getMappings());
+ foreach ($breakpoint_preview_options as $key => $value) {
+ $breakpoint = entity_load('breakpoint', $key);
+ $breakpoint_preview_options[$key] = $breakpoint->label();
+ }
+ $form['picture_mapping']['preview_' . $machine_name]['breakpoint_selection'] = array(
+ '#type' => 'select',
+ '#title' => t('Breakpoint'),
+ '#options' => $breakpoint_preview_options,
+ '#wrapper_attributes' => array('class' => array('container-block')),
+ );
+
+ // Show preview of the picture mapping at a specific breakpoint, but
+ // always at multiplier 1x to limit the complexity in the UI.
+ foreach ($picture_mapping->getMappings() as $breakpoint_machine_name => $image_style_by_multiplier) {
+ $image_style = entity_load('image_style', $image_style_by_multiplier['1x']);
+ $preview_arguments = array(
+ '#theme' => 'image_style_preview',
+ '#style' => $image_style,
+ );
+ $form['picture_mapping']['preview_' . $machine_name]['breakpoint_' . $breakpoint_machine_name] = array(
+ '#type' => 'item',
+ '#markup' => drupal_render($preview_arguments),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="picture_mapping[preview_toggle]"]' => array('checked' => TRUE),
+ ':input[name="attributes[data-picture-mapping]"]' => array('value' => $machine_name),
+ ':input[name="picture_mapping[preview_' . $machine_name . '][breakpoint_selection]"]' => array('value' => $breakpoint_machine_name),
+ ),
+ ),
+ );
+ }
+ }
+ }
+ $form['#attached']['css'][drupal_get_path('module', 'image') . '/css/image.admin.css'] = array();
+
+ $form['actions']['save_modal']['#validate'][] = 'responsive_image_form_editor_image_dialog_validate';
+ }
+}
+
+/**
+ * Form validation handler for EditorImageDialog.
+ *
+ * Ensures the image shown in the text editor matches the chosen picture mapping
+ * at the smallest breakpoint.
+ *
+ * @see \Drupal\editor\Form\EditorImageDialog::buildForm()
+ * @see \Drupal\editor\Form\EditorImageDialog::validateForm()
+ * @see picture_form_editor_image_dialog_alter()
+ */
+function responsive_image_form_editor_image_dialog_validate(array &$form, array &$form_state) {
+ $attributes = &$form_state['values']['attributes'];
+ if (!empty($form_state['values']['fid'][0])) {
+ $picture_mapping = entity_load('responsive_image_mapping', $attributes['data-picture-mapping']);
+ $file = File::load($form_state['values']['fid'][0]);
+ $uri = $file->getFileUri();
+
+ // Select the first (i.e. smallest) breakpoint and the 1x multiplier. We
+ // choose to show the image in the editor as if it were being viewed in the
+ // narrowest viewport, so that when the user starts to edit this content
+ // again on a mobile device, it will work fine.
+ $breakpoint_machine_names = array_keys($picture_mapping->getMappings());
+ $image_style = entity_load('image_style', $picture_mapping->getMappings()[$breakpoint_machine_names[0]]['1x']);
+
+ // Set the 'src' attribute to the image style URL. FilterImageStyle will
+ // look at the 'data-editor-file-uuid' attribute, not the 'src' attribute to
+ // render the appropriate output.
+ $attributes['src'] = $image_style->buildUrl($uri);
+
+ // Set the 'width' and 'height' attributes to the image style's transformed
+ // dimensions.
+ $image = \Drupal::service('image.factory')->get($uri);
+
+ if ($image->isValid()) {
+ $dimensions = array(
+ 'width' => $image->getWidth(),
+ 'height' => $image->getHeight(),
+ );
+ $image_style->transformDimensions($dimensions);
+ $attributes['width'] = $dimensions['width'];
+ $attributes['height'] = $dimensions['height'];
+ }
+ }
+}
diff --git a/core/modules/system/css/system.module.css b/core/modules/system/css/system.module.css
index 74bc83f..e343054 100644
--- a/core/modules/system/css/system.module.css
+++ b/core/modules/system/css/system.module.css
@@ -264,6 +264,10 @@ tr .ajax-progress-throbber .throbber {
.container-inline .details-wrapper {
display: block;
}
+/* Allow items inside inline items to render themselves as blocks. */
+.container-inline .container-block {
+ display: block;
+}
/**
* Prevent text wrapping.
diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css
index 8fa14dd..9194060 100644
--- a/core/modules/system/css/system.theme.css
+++ b/core/modules/system/css/system.theme.css
@@ -119,10 +119,12 @@ abbr.ajax-changed {
.container-inline .label:after {
content: ':';
}
-.form-type-radios .container-inline label:after {
+.form-type-radios .container-inline label:after,
+.container-inline .form-type-checkbox label:after {
content: '';
}
-.form-type-radios .container-inline .form-type-radio {
+.form-type-radios .container-inline .form-type-radio,
+.container-inline .form-type-checkbox {
margin: 0 1em;
}
.container-inline .form-actions,
diff --git a/core/profiles/standard/config/install/filter.format.basic_html.yml b/core/profiles/standard/config/install/filter.format.basic_html.yml
index 810a484..86107ba 100644
--- a/core/profiles/standard/config/install/filter.format.basic_html.yml
+++ b/core/profiles/standard/config/install/filter.format.basic_html.yml
@@ -14,6 +14,12 @@ filters:
allowed_html: ' -
-
-
'
filter_html_help: false
filter_html_nofollow: false
+ filter_picturemapping:
+ id: filter_picturemapping
+ module: filter
+ status: 1
+ weight: 7
+ settings: { }
filter_caption:
id: filter_caption
provider: filter
diff --git a/core/profiles/standard/config/install/filter.format.full_html.yml b/core/profiles/standard/config/install/filter.format.full_html.yml
index 9f5ea79..a8afd67 100644
--- a/core/profiles/standard/config/install/filter.format.full_html.yml
+++ b/core/profiles/standard/config/install/filter.format.full_html.yml
@@ -5,6 +5,12 @@ weight: 1
roles:
- administrator
filters:
+ filter_picturemapping:
+ id: filter_picturemapping
+ module: filter
+ status: 1
+ weight: 8
+ settings: { }
filter_caption:
id: filter_caption
provider: filter
diff --git a/core/profiles/standard/config/picture.mappings.standard_responsive_image.yml b/core/profiles/standard/config/picture.mappings.standard_responsive_image.yml
new file mode 100644
index 0000000..25af37d
--- /dev/null
+++ b/core/profiles/standard/config/picture.mappings.standard_responsive_image.yml
@@ -0,0 +1,13 @@
+id: standard_responsive_image
+uuid: 789bae80-c66c-4e38-bf00-bb450f4bac33
+label: 'Standard responsive image'
+mappings:
+ module.toolbar.narrow:
+ 1x: thumbnail
+ module.toolbar.standard:
+ 1x: medium
+ module.toolbar.wide:
+ 1x: large
+breakpointGroup: module.toolbar.toolbar
+status: true
+langcode: en
diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml
index 7029739..71e332c 100644
--- a/core/profiles/standard/standard.info.yml
+++ b/core/profiles/standard/standard.info.yml
@@ -24,6 +24,7 @@ dependencies:
- menu_ui
- options
- path
+ - responsive_image
- taxonomy
- dblog
- search