diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js old mode 100644 new mode 100755 index 0025664..cdd4dbf --- a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js +++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js @@ -60,7 +60,11 @@ element: 'img', attributes: { src: '', - alt: '' + alt: '', + width: '', + height: '', + 'data-entity-type': '', + 'data-entity-uuid': '' } }); @@ -99,7 +103,10 @@ data['data-entity-type'] = element.attributes['data-entity-type']; // Parse the data-entity-uuid attribute. data['data-entity-uuid'] = element.attributes['data-entity-uuid']; - + + data['width'] = element.attributes['width']; + data['height'] = element.attributes['height']; + return element; }; diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js old mode 100644 new mode 100755 index d1d666c..cd71a9a --- a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js +++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js @@ -58,8 +58,7 @@ requiredContent.attributes['data-align'] = ''; requiredContent.attributes['data-caption'] = ''; widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent); - widgetDefinition.allowedContent.img.attributes['!data-align'] = true; - widgetDefinition.allowedContent.img.attributes['!data-caption'] = true; + widgetDefinition.allowedContent.img.attributes += ',!data-align,!data-caption'; // Override allowedContent setting for the 'caption' nested editable. // This must match what caption_filter enforces. @@ -72,13 +71,16 @@ // data-caption attributes. var originalDowncast = widgetDefinition.downcast; widgetDefinition.downcast = function (element) { - var img = findElementByName(element, 'img'); + var img = originalDowncast.call(this, element); + if (!img) { + img = findElementByName(element, 'img'); + } originalDowncast.call(this, img); var caption = this.editables.caption; var captionHtml = caption && caption.getData(); var attrs = img.attributes; - + if (captionFilterEnabled) { // If image contains a non-empty caption, serialize caption to the // data-caption attribute. @@ -137,6 +139,7 @@ data.align = attrs['data-align']; delete attrs['data-align']; } + data['data-entity-type'] = attrs['data-entity-type']; delete attrs['data-entity-type']; data['data-entity-uuid'] = attrs['data-entity-uuid']; diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.js old mode 100644 new mode 100755 diff --git a/core/modules/editor/src/Form/EditorImageDialog.php b/core/modules/editor/src/Form/EditorImageDialog.php old mode 100644 new mode 100755 index dcacf64..486d135 --- a/core/modules/editor/src/Form/EditorImageDialog.php +++ b/core/modules/editor/src/Form/EditorImageDialog.php @@ -216,7 +216,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Transform absolute image URLs to relative image URLs: prevent problems // on multisite set-ups and prevent mixed content errors. $file_url = file_url_transform_relative($file_url); - $form_state->setValue(array('attributes', 'src'), $file_url); + if(!$form_state->getValue(array('attributes', 'src'))) { + $form_state->setValue(array('attributes', 'src'), $file_url); + } $form_state->setValue(array('attributes', 'data-entity-uuid'), $file->uuid()); $form_state->setValue(array('attributes', 'data-entity-type'), 'file'); } diff --git a/core/modules/image/css/image.admin.css b/core/modules/image/css/image.admin.css old mode 100644 new mode 100755 index 9f9878a..dd18988 --- a/core/modules/image/css/image.admin.css +++ b/core/modules/image/css/image.admin.css @@ -1,4 +1,3 @@ - /** * Image style configuration pages. */ @@ -71,4 +70,4 @@ } .image-anchor td { border: 1px solid #ccc; -} +} \ No newline at end of file diff --git a/core/modules/image/image.module b/core/modules/image/image.module old mode 100644 new mode 100755 index 7f8314a..6dff7f3 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -4,14 +4,17 @@ * @file * Exposes global functionality for creating image styles. */ - +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Form\FormStateInterface; use Drupal\file\Entity\File; use Drupal\field\FieldStorageConfigInterface; use Drupal\field\FieldConfigInterface; use Drupal\image\Entity\ImageStyle; + + /** * Image style constant for user presets in the database. * @@ -483,3 +486,107 @@ function image_field_config_delete(FieldConfigInterface $field) { \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->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, FormStateInterface $form_state) { + + $filter_format = $form_state->getBuildInfo()['args'][0]; + $filters = $filter_format->filters()->getAll(); + + $image_element = $form_state->getStorage()['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']['library'][] = 'image/admin'; + + $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, FormStateInterface &$form_state) { + $attributes =& $form_state->getValue('attributes'); + if (!empty($form_state->getValue('fid')[0])) { + $image_style = entity_load('image_style', $attributes['data-image-style']); + $file = file_load($form_state->getValue('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->isValid()) { + $dimensions = array( + 'width' => $image->getWidth(), + 'height' => $image->getHeight() + ); + $image_style->transformDimensions($dimensions, $attributes['src']); + $attributes['width'] = $dimensions['width']; + $attributes['height'] = $dimensions['height']; + } + } +} \ No newline at end of file diff --git a/core/modules/image/js/plugins/drupalimagestyle/plugin.js b/core/modules/image/js/plugins/drupalimagestyle/plugin.js new file mode 100755 index 0000000..ed0730f --- /dev/null +++ b/core/modules/image/js/plugins/drupalimagestyle/plugin.js @@ -0,0 +1,111 @@ +/** + * @file + * Drupal Image Style plugin. + * + * This alters the existing CKEditor image2 widget plugin, which is already + * altered by the Drupal Image plugin, to allow for the data-image-style + * attribute to be set. + * + * @ignore + */ + +(function (CKEDITOR) { + + "use strict"; + + CKEDITOR.plugins.add('drupalimagestyle', { + requires: 'drupalimage', + + beforeInit: function (editor) { + // Override the image2 widget definition to handle the additional + // data-image-style attributes. + editor.on('widgetDefinition', function (event) { + var widgetDefinition = event.data; + if (widgetDefinition.name !== 'image') { + return; + } + // Override default features definitions for drupalimagestyle. + CKEDITOR.tools.extend(widgetDefinition.features, { + responsiveimage: { + requiredContent: 'img[data-image-style]' + } + }, true); + + // Override requiredContent & allowedContent. + var requiredContent = widgetDefinition.requiredContent.getDefinition(); + requiredContent.attributes['data-image-style'] = ''; + widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent); + widgetDefinition.allowedContent.img.attributes += ',!data-image-style'; + + // Override downcast(). + var originalDowncast = widgetDefinition.downcast; + + widgetDefinition.downcast = function (element) { + var img = originalDowncast.call(this, element); + if (!img) { + img = findElementByName(element, 'img'); + } + img.attributes['data-image-style'] = this.data['data-image-style']; + return img; + }; + + // Override upcast(). + var originalUpcast = widgetDefinition.upcast; + widgetDefinition.upcast = function (element, data) { + if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) { + return; + } + // Don't initialize on pasted fake objects. + else if (element.attributes['data-cke-realelement']) { + return; + } + element = originalUpcast.call(this, element, data); + + // Parse the data-image-style attribute. + data['data-image-style'] = element.attributes['data-image-style']; + + return element; + }; + + // Protected; keys of the widget data to be sent to the Drupal dialog. + // Append to the values defined by the drupalimage plugin. + // @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js + CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, { + 'data-image-style': 'data-image-style' + }); + // Low priority to ensure drupalimage's event handler runs first. + }, null, null, 20); + } + }); + + /** + * Finds an element by its name. + * + * Function will check first the passed element itself and then all its + * children in DFS order. + * + * @param {CKEDITOR.htmlParser.element} element + * The element to search. + * @param {string} name + * The element name to search for. + * + * @return {?CKEDITOR.htmlParser.element} + * The found element, or null. + */ + function findElementByName(element, name) { + if (element.name === name) { + return element; + } + + var found = null; + element.forEach(function (el) { + if (el.name === name) { + found = el; + // Stop here. + return false; + } + }, CKEDITOR.NODE_ELEMENT); + return found; + } + +})(CKEDITOR); \ No newline at end of file diff --git a/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php new file mode 100755 index 0000000..5919119 --- /dev/null +++ b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php @@ -0,0 +1,91 @@ +hasAssociatedFilterFormat()) { + return FALSE; + } + + // Automatically enable this plugin if the text format associated with this + // text editor uses the filter_imagestyle filter and the DrupalImage button + // is enabled. + $format = $editor->getFilterFormat(); + if ($format->filters('filter_imagestyle')->status) { + $enabled = FALSE; + $settings = $editor->getSettings(); + foreach ($settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + foreach ($group['items'] as $button) { + if ($button === 'DrupalImage') { + $enabled = TRUE; + } + } + } + } + return $enabled; + } + + return FALSE; + } + +} \ No newline at end of file diff --git a/core/modules/image/src/Plugin/Filter/FilterImageStyle.php b/core/modules/image/src/Plugin/Filter/FilterImageStyle.php new file mode 100755 index 0000000..f93cf91 --- /dev/null +++ b/core/modules/image/src/Plugin/Filter/FilterImageStyle.php @@ -0,0 +1,119 @@ +query('//*[@data-entity-type="file" and @data-entity-uuid and @data-image-style]') as $node) { + $file_uuid = $node->getAttribute('data-entity-uuid'); + $node->removeAttribute('data-entity-uuid'); + $image_style_id = $node->getAttribute('data-image-style'); + $node->removeAttribute('data-image-style'); + + // If the image style is not a valid one, then don't transform the HTML. + if (empty($file_uuid) || !in_array($image_style_id, array_keys($image_styles))) { + continue; + } + + $file = \Drupal::entityManager()->loadEntityByUuid('file', $file_uuid); + + // Determine width/height of the source image. + $width = $height = NULL; + $image = \Drupal::service('image.factory')->get($file->getFileUri()); + if ($image->isValid()) { + $width = $image->getWidth(); + $height = $image->getHeight(); + } + + // Make sure all non-regenerated attributes are retained. + $node->removeAttribute('width'); + $node->removeAttribute('height'); + $node->removeAttribute('src'); + $attributes = array(); + for ($i = 0; $i < $node->attributes->length; $i++) { + $attr = $node->attributes->item($i); + $attributes[$attr->name] = $attr->value; + } + + // Re-render as an image style. + $image = array( + '#theme' => 'image_style', + '#style_name' => $image_style_id, + '#uri' => $file->getFileUri(), + '#width' => $width, + '#height' => $height, + '#attributes' => $attributes, + ); + $altered_html = drupal_render($image); + + // Load the altered HTML into a new DOMDocument and retrieve the element. + $updated_node = HTML::load($altered_html)->getElementsByTagName('body') + ->item(0) + ->childNodes + ->item(0); + + // Import the updated node from the new DOMDocument into the original + // one, importing also the child nodes of the updated node. + $updated_node = $dom->importNode($updated_node, TRUE); + // Finally, replace the original image node with the new image node! + $node->parentNode->replaceChild($updated_node, $node); + } + + return new FilterProcessResult(HTML::serialize($dom)); + } + + return new FilterProcessResult($text); + } + + /** + * {@inheritdoc} + */ + public function tips($long = FALSE) { + if ($long) { + $image_styles = entity_load_multiple('image_style'); + $list = '' . implode(', ', array_keys($image_styles)) . ''; + return t(' +

You can display images using site-wide styles by adding a data-image-style attribute, whose values is one of the image style machine names: !image-style-machine-name-list.

', array('!image-style-machine-name-list' => $list)); + } + else { + return t('You can display images using site-wide styles by adding a data-image-style attribute.'); + } + } +} \ No newline at end of file diff --git a/core/modules/image/templates/image-style-preview.html.twig b/core/modules/image/templates/image-style-preview.html.twig old mode 100644 new mode 100755 index d6e715c..f3cc968 --- a/core/modules/image/templates/image-style-preview.html.twig +++ b/core/modules/image/templates/image-style-preview.html.twig @@ -33,7 +33,7 @@
{# Preview of the original image. #}
- {{ 'original'|t }} ({{ 'view actual size'|t }}) + {{ 'original'|t }} ({{ 'view actual size'|t }})
{{ original.rendered }} @@ -45,7 +45,7 @@ {# Derivative of the image style. #}
- {{ style_name }} ({{ 'view actual size'|t }}) + {{ style_name }} ({{ 'view actual size'|t }})
{{ derivative.rendered }} diff --git a/core/modules/responsive_image/js/plugins/drupalresponsiveimagestyle/plugin.js b/core/modules/responsive_image/js/plugins/drupalresponsiveimagestyle/plugin.js new file mode 100755 index 0000000..161edfe --- /dev/null +++ b/core/modules/responsive_image/js/plugins/drupalresponsiveimagestyle/plugin.js @@ -0,0 +1,111 @@ +/** + * @file + * Drupal Responsive Image Style plugin. + * + * This alters the existing CKEditor image2 widget plugin, which is already + * altered by the Drupal Image plugin, to data-responsive-image-style attribute + * to be set. + * + * @ignore + */ + +(function (CKEDITOR) { + + "use strict"; + + CKEDITOR.plugins.add('drupalresponsiveimagestyle', { + requires: 'drupalimage', + + beforeInit: function (editor) { + // Override the image2 widget definition to handle the additional + // data-responsive-image-style attributes. + editor.on('widgetDefinition', function (event) { + var widgetDefinition = event.data; + if (widgetDefinition.name !== 'image') { + return; + } + // Override default features definitions for drupalresponsiveimagestyle. + CKEDITOR.tools.extend(widgetDefinition.features, { + responsiveimage: { + requiredContent: 'img[data-responsive-image-style]' + } + }, true); + + // Override requiredContent & allowedContent. + var requiredContent = widgetDefinition.requiredContent.getDefinition(); + requiredContent.attributes['data-responsive-image-style'] = ''; + widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent); + widgetDefinition.allowedContent.img.attributes += ',!data-responsive-image-style'; + + // Override downcast(). + var originalDowncast = widgetDefinition.downcast; + widgetDefinition.downcast = function (element) { + var img = originalDowncast.call(this, element); + if (!img) { + img = findElementByName(element, 'img'); + } + img.attributes['data-responsive-image-style'] = this.data['data-responsive-image-style']; + + return img; + }; + + // Override upcast(). + var originalUpcast = widgetDefinition.upcast; + widgetDefinition.upcast = function (element, data) { + if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) { + return; + } + // Don't initialize on pasted fake objects. + else if (element.attributes['data-cke-realelement']) { + return; + } + element = originalUpcast.call(this, element, data); + + // Parse the data-responsive-image-style attribute. + data['data-responsive-image-style'] = element.attributes['data-responsive-image-style']; + + return element; + }; + + // Protected; keys of the widget data to be sent to the Drupal dialog. + // Append to the values defined by the drupalimage plugin. + // @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js + CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, { + 'data-responsive-image-style': 'data-responsive-image-style', + }); + // Low priority to ensure drupalimage's event handler runs first. + }, null, null, 20); + } + }); + + /** + * Finds an element by its name. + * + * Function will check first the passed element itself and then all its + * children in DFS order. + * + * @param {CKEDITOR.htmlParser.element} element + * The element to search. + * @param {string} name + * The element name to search for. + * + * @return {?CKEDITOR.htmlParser.element} + * The found element, or null. + */ + function findElementByName(element, name) { + if (element.name === name) { + return element; + } + + var found = null; + element.forEach(function (el) { + if (el.name === name) { + found = el; + // Stop here. + return false; + } + }, CKEDITOR.NODE_ELEMENT); + return found; + } + +})(CKEDITOR); \ No newline at end of file diff --git a/core/modules/responsive_image/responsive_image.install b/core/modules/responsive_image/responsive_image.install new file mode 100755 index 0000000..532a580 --- /dev/null +++ b/core/modules/responsive_image/responsive_image.install @@ -0,0 +1,27 @@ +listAll('filter.format') as $name) { + $config = $config_factory->getEditable($name); + if (!$config->get('filters.filter_responsive_image_style.status')) { + continue; + } + $allowed_html = $config->get('filters.filter_html.settings.allowed_html'); + $matches = []; + if (!empty($allowed_html) && preg_match('/]*)>/', $allowed_html, $matches)) { + $new_attributes = array_filter(explode(' ', $matches[1])); + $new_attributes[] = 'data-responsive-image-style'; + $allowed_html = preg_replace('/]*)>/', '', $allowed_html); + $config->set('filters.filter_html.settings.allowed_html', $allowed_html); + $config->save(); + } + } +} \ No newline at end of file diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module old mode 100644 new mode 100755 index 7df1da8..20e1919 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -11,6 +11,8 @@ use Drupal\responsive_image\Entity\ResponsiveImageStyle; use Drupal\Core\Image\ImageInterface; use Drupal\breakpoint\BreakpointInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\file\Entity\File; /** * The machine name for the empty image breakpoint image style option. @@ -510,3 +512,172 @@ function responsive_image_library_info_alter(array &$libraries, $module) { $libraries['drupal.ajax']['dependencies'][] = 'responsive_image/ajax'; } } + + +/** + * Implements hook_form_FORM_ID_alter() for EditorImageDialog. + * + * Alters the image dialog form for text editor, to allow the user to select a + * responsive image style. + * + * @see \Drupal\editor\Form\EditorImageDialog::buildForm() + */ +function responsive_image_form_editor_image_dialog_alter(&$form, FormStateInterface $form_state) { + + $filter_format = $form_state->getBuildInfo()['args'][0]; + $filters = $filter_format->filters()->getAll(); + + $image_element = $form_state->getStorage()['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 responsive image style to be selected. + if (isset($filters['filter_responsive_image_style']) && $filters['filter_responsive_image_style']->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['responsive_image_style'] = array( + '#type' => 'item', + ); + $responsive_image_options = array(); + $responsive_image_styles = entity_load_multiple('responsive_image_style'); + if ($responsive_image_styles && !empty($responsive_image_styles)) { + foreach ($responsive_image_styles as $responsive_image_style_id => $responsive_image_style) { + if ($responsive_image_style->hasImageStyleMappings()) { + $responsive_image_options[$responsive_image_style_id] = $responsive_image_style->label(); + } + } + } + + $form['responsive_image_style']['selection'] = array( + '#title' => t('Responsive image style'), + '#type' => 'select', + '#default_value' => isset($image_element['data-responsive-image-style']) ? $image_element['data-responsive-image-style'] : key($responsive_image_options), + '#options' => $responsive_image_options, + '#required' => TRUE, + '#wrapper_attributes' => array('class' => array('container-inline')), + '#attributes' => array('class' => array('container-inline')), + '#parents' => array('attributes', 'data-responsive-image-style'), + ); + $form['responsive_image_style']['preview_toggle'] = array( + '#type' => 'checkbox', + '#title' => t('Show preview'), + ); + foreach ($responsive_image_styles as $responsive_image_style_id => $responsive_image_style) { + if ($responsive_image_style->hasImageStyleMappings()) { + $form['responsive_image_style']['preview_' . $responsive_image_style_id] = array( + '#type' => 'fieldset', + '#title' => t('Preview of %responsive-image-style responsive image style', array('%responsive-image-style' => $responsive_image_style->label())), + '#states' => array( + 'visible' => array( + ':input[name="responsive_image_style[preview_toggle]"]' => array('checked' => TRUE), + ':input[name="attributes[data-responsive-image-style]"]' => array('value' => $responsive_image_style_id), + ), + ), + ); + + $preview_arguments = array( + '#theme' => 'responsive_image', + '#uri' => \Drupal::config('image.settings')->get('preview_image'), + '#responsive_image_style_id' => $responsive_image_style_id, + ); + $form['responsive_image_style']['preview_' . $responsive_image_style_id] = array( + '#type' => 'item', + '#markup' => drupal_render($preview_arguments), + '#states' => array( + 'visible' => array( + ':input[name="responsive_image_style[preview_toggle]"]' => array('checked' => TRUE), + ':input[name="attributes[data-responsive-image-style]"]' => array('value' => $responsive_image_style_id), + ), + ), + ); + } + } + + $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, FormStateInterface $form_state) { + $attributes = &$form_state->getValue('attributes'); + if (!empty($form_state->getValue('fid')[0])) { + $responsive_image_style = entity_load('responsive_image_style', $attributes['data-responsive-image-style']); + $file = File::load($form_state->getValue('fid')[0]); + $uri = $file->getFileUri(); + // Set up original file information. + $image = \Drupal::service('image.factory')->get($uri); + if ($image->isValid()) { + $dimensions = [ + 'width' => $image->getWidth(), + 'height' => $image->getHeight(), + ]; + } + else { + // @todo: what if the image is not valid? + $dimensions = [ + 'width' => 1000, + 'height' => 1000, + ]; + } + + // 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($responsive_image_style->getKeyedImageStyleMappings()); + $image_style_mapping = $responsive_image_style->getKeyedImageStyleMappings()[$breakpoint_machine_names[0]]; + switch ($image_style_mapping['image_mapping_type']) { + case 'sizes': + // More than one image style can be mapped. Use the smallest one. + $transformed_dimensions = $dimensions; + foreach ($image_style_mapping['image_mapping']['sizes_image_styles'] as $mapped_image_style) { + $new_dimensions = responsive_image_get_image_dimensions($mapped_image_style, $dimensions, $uri); + if (!$transformed_dimensions || $transformed_dimensions['width'] >= $new_dimensions['width']) { + $image_style_name = $mapped_image_style; + $transformed_dimensions = $new_dimensions; + } + } + break; + + case 'image_style': + $image_style_name = $image_style_mapping['image_mapping']; + $transformed_dimensions = responsive_image_get_image_dimensions($image_style_name, $dimensions, $uri); + break; + } + + // 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'] = _responsive_image_image_style_url($image_style_name, $uri); + + // Set the 'width' and 'height' attributes to the image style's transformed + // dimensions. + if ($image->isValid()) { + $attributes['width'] = $transformed_dimensions['width']; + $attributes['height'] = $transformed_dimensions['height']; + } + } +} \ No newline at end of file diff --git a/core/modules/responsive_image/src/Plugin/CKEditorPlugin/DrupalResponsiveImageStyle.php b/core/modules/responsive_image/src/Plugin/CKEditorPlugin/DrupalResponsiveImageStyle.php new file mode 100755 index 0000000..df159b2 --- /dev/null +++ b/core/modules/responsive_image/src/Plugin/CKEditorPlugin/DrupalResponsiveImageStyle.php @@ -0,0 +1,91 @@ +hasAssociatedFilterFormat()) { + return FALSE; + } + + // Automatically enable this plugin if the text format associated with this + // text editor uses the filter_responsive_image_style filter and the + // DrupalImage button is enabled. + $format = $editor->getFilterFormat(); + if ($format->filters('filter_responsive_image_style')->status) { + $enabled = FALSE; + $settings = $editor->getSettings(); + foreach ($settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + foreach ($group['items'] as $button) { + if ($button === 'DrupalImage') { + $enabled = TRUE; + } + } + } + } + return $enabled; + } + + return FALSE; + } + +} \ No newline at end of file diff --git a/core/modules/responsive_image/src/Plugin/Filter/FilterResponsiveImageStyle.php b/core/modules/responsive_image/src/Plugin/Filter/FilterResponsiveImageStyle.php new file mode 100755 index 0000000..b8c6eda --- /dev/null +++ b/core/modules/responsive_image/src/Plugin/Filter/FilterResponsiveImageStyle.php @@ -0,0 +1,123 @@ +query('//*[@data-entity-uuid and @data-responsive-image-style]') as $node) { + $file_uuid = $node->getAttribute('data-entity-uuid'); + $node->removeAttribute('data-entity-uuid'); + $responsive_image_style_id = $node->getAttribute('data-responsive-image-style'); + $node->removeAttribute('data-responsive-image-style'); + + // If the responsive image style is not a valid one, then don't + // transform the HTML. + if (empty($file_uuid) || !in_array($responsive_image_style_id, array_keys($responsive_image_styes))) { + continue; + } + + $file = \Drupal::entityManager()->loadEntityByUuid('file', $file_uuid); + + // Determine width/height of images that don't have such attributes set. + $width = $node->getAttribute('width'); + $height = $node->getAttribute('height'); + if (empty($width) || empty($height)) { + $image = \Drupal::service('image.factory')->get($file->getFileUri()); + if ($image->isValid()) { + if (empty($width)) { + $width = $image->getWidth(); + } + if (empty($height)) { + $height = $image->getHeight(); + } + } + } + + // Make sure all non-regenerated attributes are retained. + $node->removeAttribute('width'); + $node->removeAttribute('height'); + $node->removeAttribute('src'); + $attributes = array(); + for ($i = 0; $i < $node->attributes->length; $i++) { + $attr = $node->attributes->item($i); + $attributes[$attr->name] = $attr->value; + } + + // Re-render as a responsive image. + $responsive_image = array( + '#theme' => 'responsive_image', + '#uri' => $file->getFileUri(), + '#width' => $width, + '#height' => $height, + '#attributes' => $attributes, + '#responsive_image_style_id' => $responsive_image_style_id, + ); + $altered_html = drupal_render($responsive_image); + + // Load the altered HTML into a new DOMDocument and retrieve the element. + $updated_node = Html::load(trim($altered_html))->getElementsByTagName('body') + ->item(0) + ->childNodes + ->item(0); + + // Import the updated node from the new DOMDocument into the original + // one, importing also the child nodes of the updated node. + $updated_node = $dom->importNode($updated_node, TRUE); + // Finally, replace the original image node with the new image node! + $node->parentNode->replaceChild($updated_node, $node); + } + + return new FilterProcessResult(Html::serialize($dom)); + } + + return new FilterProcessResult($text); + } + + /** + * {@inheritdoc} + */ + public function tips($long = FALSE) { + if ($long) { + $responsive_image_styles = entity_load_multiple('responsive_image_style'); + $list = '' . implode(', ', array_keys($responsive_image_styles)) . ''; + return t(' +

You can make images responsive by adding a data-responsive-image-style attribute, whose values is one of the responsive image style machine names: !responsive-image-style-machine-name-list.

', array('!responsive-image-style-machine-name-list' => $list)); + } + else { + return t('You can make images responsive by adding a data-responsive-image-style attribute.'); + } + } +} \ No newline at end of file diff --git a/core/modules/system/css/components/container-inline.module.css b/core/modules/system/css/components/container-inline.module.css old mode 100644 new mode 100755 index b68da65..8083ff0 --- a/core/modules/system/css/components/container-inline.module.css +++ b/core/modules/system/css/components/container-inline.module.css @@ -11,3 +11,7 @@ .container-inline .details-wrapper { display: block; } +/* Allow items inside inline items to render themselves as blocks. */ +.container-inline .container-block { + display: block; +} \ No newline at end of file diff --git a/core/modules/system/system.install b/core/modules/system/system.install old mode 100644 new mode 100755 index 44212b1..e5e8ee7 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1644,5 +1644,27 @@ function system_update_8014() { } /** + * Add allowed attributes to existing html filters. + */ +function system_update_8012() { + $config_factory = \Drupal::configFactory(); + foreach ($config_factory->listAll('filter.format') as $name) { + $config = $config_factory->getEditable($name); + if (!$config->get('filters.filter_imagestyle.status')) { + continue; + } + $allowed_html = $config->get('filters.filter_html.settings.allowed_html'); + $matches = []; + if (!empty($allowed_html) && preg_match('/]*)>/', $allowed_html, $matches)) { + $new_attributes = array_filter(explode(' ', $matches[1])); + $new_attributes[] = 'data-image-style'; + $allowed_html = preg_replace('/]*)>/', '', $allowed_html); + $config->set('filters.filter_html.settings.allowed_html', $allowed_html); + $config->save(); + } + } +} + +/** * @} End of "addtogroup updates-8.0.0-rc". */ diff --git a/core/profiles/standard/config/install/filter.format.basic_html.yml b/core/profiles/standard/config/install/filter.format.basic_html.yml old mode 100644 new mode 100755 index 92224c2..63d649a --- a/core/profiles/standard/config/install/filter.format.basic_html.yml +++ b/core/profiles/standard/config/install/filter.format.basic_html.yml @@ -15,7 +15,7 @@ filters: status: true weight: -10 settings: - allowed_html: '