diff --cc core/modules/responsive_image/responsive_image.module index ca7a3ed,cfed1dc..0000000 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@@ -6,14 -6,10 +6,14 @@@ */ use Drupal\Component\Utility\SafeMarkup; -use Drupal\Core\Form\FormStateInterface; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Routing\RouteMatchInterface; --use \Drupal\Core\Template\Attribute; +use Drupal\image\Entity\ImageStyle; - use Drupal\Core\Url; +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. @@@ -471,12 -326,168 +471,173 @@@ function responsive_image_get_mime_type function _responsive_image_image_style_url($style_name, $path) { if ($style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) { // The smallest data URI for a 1px square transparent GIF image. - return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever + return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; + } + $entity = ImageStyle::load($style_name); + if ($entity instanceof Drupal\image\Entity\ImageStyle) { + return $entity->buildUrl($path); } - return entity_load('image_style', $style_name)->buildUrl($path); + return file_create_url($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, 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 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()); + $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 $mapping) { + if ($mapping['multiplier'] == '1x') { + $breakpoint_id = $mapping['breakpoint_id']; + $image_style = entity_load('image_style', $mapping['image_style']); + $preview_arguments = array( + '#theme' => 'image_style_preview', + '#style' => $image_style, + ); + $form['picture_mapping']['preview_' . $machine_name]['breakpoint_' . $breakpoint_id] = 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), ++ ':input[name="picture_mapping[preview_' . $machine_name . '][breakpoint_selection]"]' => array('value' => $breakpoint_id), + ), + ), + ); + } + } + } + } + $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, FormStateInterface $form_state) { + $attributes = &$form_state->getValue('attributes'); + if (!empty($form_state->getValue('fid')[0])) { + $picture_mapping = entity_load('responsive_image_mapping', $attributes['data-picture-mapping']); + $file = File::load($form_state->getValue('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->getKeyedMappings()); + $image_style = entity_load('image_style', $picture_mapping->getKeyedMappings()[$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 --cc core/modules/system/css/system.module.css index 0359e4b,579b608..0000000 --- a/core/modules/system/css/system.module.css +++ b/core/modules/system/css/system.module.css @@@ -280,32 -280,10 +280,36 @@@ tr .ajax-progress-throbber .throbber .container-inline .details-wrapper { display: block; } +/* Display form elements horizontally. */ +.form--inline .form-item { + float: left; /* LTR */ + margin-right: 0.5em; /* LTR */ +} +[dir="rtl"] .form--inline .form-item { + float: right; + margin-right: 0; + margin-left: 0.5em; +} +.form--inline .form-item-separator { + margin-top: 2.3em; + margin-right: 1em; /* LTR */ + margin-left: 0.5em; /* LTR */ +} +[dir="rtl"] .form--inline .form-item-separator { + margin-right: 0.5em; + margin-left: 1em; +} +.form--inline .form-actions { + clear: left; /* LTR */ +} +[dir="rtl"] .form--inline .form-actions { + clear: right; +} + + /* Allow items inside inline items to render themselves as blocks. */ + .container-inline .container-block { + display: block; + } /** * Prevent text wrapping. diff --cc core/profiles/standard/standard.info.yml index a356ae8,1892042..0000000 --- a/core/profiles/standard/standard.info.yml +++ b/core/profiles/standard/standard.info.yml @@@ -25,7 -25,7 +25,8 @@@ dependencies - menu_ui - options - path + - page_cache + - responsive_image - taxonomy - dblog - search