3 namespace Drupal\image\Plugin\Field\FieldWidget;
5 use Drupal\Core\Field\FieldItemListInterface;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\file\Entity\File;
9 use Drupal\file\Plugin\Field\FieldWidget\FileWidget;
10 use Drupal\image\Entity\ImageStyle;
13 * Plugin implementation of the 'image_image' widget.
17 * label = @Translation("Image"),
23 class ImageWidget extends FileWidget {
28 public static function defaultSettings() {
30 'progress_indicator' => 'throbber',
31 'preview_image_style' => 'thumbnail',
32 ] + parent::defaultSettings();
38 public function settingsForm(array $form, FormStateInterface $form_state) {
39 $element = parent::settingsForm($form, $form_state);
41 $element['preview_image_style'] = [
42 '#title' => t('Preview image style'),
44 '#options' => image_style_options(FALSE),
45 '#empty_option' => '<' . t('no preview') . '>',
46 '#default_value' => $this->getSetting('preview_image_style'),
47 '#description' => t('The preview image will be shown while editing the content.'),
57 public function settingsSummary() {
58 $summary = parent::settingsSummary();
60 $image_styles = image_style_options(FALSE);
61 // Unset possible 'No defined styles' option.
62 unset($image_styles['']);
63 // Styles could be lost because of enabled/disabled modules that defines
64 // their styles in code.
65 $image_style_setting = $this->getSetting('preview_image_style');
66 if (isset($image_styles[$image_style_setting])) {
67 $preview_image_style = t('Preview image style: @style', ['@style' => $image_styles[$image_style_setting]]);
70 $preview_image_style = t('No preview');
73 array_unshift($summary, $preview_image_style);
79 * Overrides \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formMultipleElements().
81 * Special handling for draggable multiple widgets and 'add more' button.
83 protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
84 $elements = parent::formMultipleElements($items, $form, $form_state);
86 $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
88 '#theme' => 'file_upload_help',
90 '#upload_validators' => $elements[0]['#upload_validators'],
91 '#cardinality' => $cardinality,
93 if ($cardinality == 1) {
94 // If there's only one field, return it as delta 0.
95 if (empty($elements[0]['#default_value']['fids'])) {
96 $file_upload_help['#description'] = $this->getFilteredDescription();
97 $elements[0]['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help);
101 $elements['#file_upload_description'] = $file_upload_help;
110 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
111 $element = parent::formElement($items, $delta, $element, $form, $form_state);
113 $field_settings = $this->getFieldSettings();
115 // Add upload resolution validation.
116 if ($field_settings['max_resolution'] || $field_settings['min_resolution']) {
117 $element['#upload_validators']['file_validate_image_resolution'] = [$field_settings['max_resolution'], $field_settings['min_resolution']];
120 // If not using custom extension validation, ensure this is an image.
121 $supported_extensions = ['png', 'gif', 'jpg', 'jpeg'];
122 $extensions = isset($element['#upload_validators']['file_validate_extensions'][0]) ? $element['#upload_validators']['file_validate_extensions'][0] : implode(' ', $supported_extensions);
123 $extensions = array_intersect(explode(' ', $extensions), $supported_extensions);
124 $element['#upload_validators']['file_validate_extensions'][0] = implode(' ', $extensions);
126 // Add mobile device image capture acceptance.
127 $element['#accept'] = 'image/*';
129 // Add properties needed by process() method.
130 $element['#preview_image_style'] = $this->getSetting('preview_image_style');
131 $element['#title_field'] = $field_settings['title_field'];
132 $element['#title_field_required'] = $field_settings['title_field_required'];
133 $element['#alt_field'] = $field_settings['alt_field'];
134 $element['#alt_field_required'] = $field_settings['alt_field_required'];
137 $default_image = $field_settings['default_image'];
138 if (empty($default_image['uuid'])) {
139 $default_image = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('default_image');
141 // Convert the stored UUID into a file ID.
142 if (!empty($default_image['uuid']) && $entity = \Drupal::entityManager()->loadEntityByUuid('file', $default_image['uuid'])) {
143 $default_image['fid'] = $entity->id();
145 $element['#default_image'] = !empty($default_image['fid']) ? $default_image : [];
151 * Form API callback: Processes a image_image field element.
153 * Expands the image_image type to include the alt and title fields.
155 * This method is assigned as a #process callback in formElement() method.
157 public static function process($element, FormStateInterface $form_state, $form) {
158 $item = $element['#value'];
159 $item['fids'] = $element['fids']['#value'];
161 $element['#theme'] = 'image_widget';
163 // Add the image preview.
164 if (!empty($element['#files']) && $element['#preview_image_style']) {
165 $file = reset($element['#files']);
167 'style_name' => $element['#preview_image_style'],
168 'uri' => $file->getFileUri(),
171 // Determine image dimensions.
172 if (isset($element['#value']['width']) && isset($element['#value']['height'])) {
173 $variables['width'] = $element['#value']['width'];
174 $variables['height'] = $element['#value']['height'];
177 $image = \Drupal::service('image.factory')->get($file->getFileUri());
178 if ($image->isValid()) {
179 $variables['width'] = $image->getWidth();
180 $variables['height'] = $image->getHeight();
183 $variables['width'] = $variables['height'] = NULL;
187 $element['preview'] = [
189 '#theme' => 'image_style',
190 '#width' => $variables['width'],
191 '#height' => $variables['height'],
192 '#style_name' => $variables['style_name'],
193 '#uri' => $variables['uri'],
196 // Store the dimensions in the form so the file doesn't have to be
197 // accessed again. This is important for remote files.
198 $element['width'] = [
200 '#value' => $variables['width'],
202 $element['height'] = [
204 '#value' => $variables['height'],
207 elseif (!empty($element['#default_image'])) {
208 $default_image = $element['#default_image'];
209 $file = File::load($default_image['fid']);
211 $element['preview'] = [
213 '#theme' => 'image_style',
214 '#width' => $default_image['width'],
215 '#height' => $default_image['height'],
216 '#style_name' => $element['#preview_image_style'],
217 '#uri' => $file->getFileUri(),
222 // Add the additional alt and title fields.
224 '#title' => t('Alternative text'),
225 '#type' => 'textfield',
226 '#default_value' => isset($item['alt']) ? $item['alt'] : '',
227 '#description' => t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'),
228 // @see https://www.drupal.org/node/465106#alt-text
231 '#access' => (bool) $item['fids'] && $element['#alt_field'],
232 '#required' => $element['#alt_field_required'],
233 '#element_validate' => $element['#alt_field_required'] == 1 ? [[get_called_class(), 'validateRequiredFields']] : [],
235 $element['title'] = [
236 '#type' => 'textfield',
237 '#title' => t('Title'),
238 '#default_value' => isset($item['title']) ? $item['title'] : '',
239 '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'),
240 '#maxlength' => 1024,
242 '#access' => (bool) $item['fids'] && $element['#title_field'],
243 '#required' => $element['#title_field_required'],
244 '#element_validate' => $element['#title_field_required'] == 1 ? [[get_called_class(), 'validateRequiredFields']] : [],
247 return parent::process($element, $form_state, $form);
251 * Validate callback for alt and title field, if the user wants them required.
253 * This is separated in a validate function instead of a #required flag to
254 * avoid being validated on the process callback.
256 public static function validateRequiredFields($element, FormStateInterface $form_state) {
257 // Only do validation if the function is triggered from other places than
258 // the image process form.
259 if (!in_array('file_managed_file_submit', $form_state->getTriggeringElement()['#submit'])) {
260 // If the image is not there, we do not check for empty values.
261 $parents = $element['#parents'];
262 $field = array_pop($parents);
263 $image_field = NestedArray::getValue($form_state->getUserInput(), $parents);
264 // We check for the array key, so that it can be NULL (like if the user
265 // submits the form without using the "upload" button).
266 if (!array_key_exists($field, $image_field)) {
271 $form_state->setLimitValidationErrors([]);
278 public function calculateDependencies() {
279 $dependencies = parent::calculateDependencies();
280 $style_id = $this->getSetting('preview_image_style');
281 /** @var \Drupal\image\ImageStyleInterface $style */
282 if ($style_id && $style = ImageStyle::load($style_id)) {
283 // If this widget uses a valid image style to display the preview of the
284 // uploaded image, add that image style configuration entity as dependency
286 $dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName();
288 return $dependencies;
294 public function onDependencyRemoval(array $dependencies) {
295 $changed = parent::onDependencyRemoval($dependencies);
296 $style_id = $this->getSetting('preview_image_style');
297 /** @var \Drupal\image\ImageStyleInterface $style */
298 if ($style_id && $style = ImageStyle::load($style_id)) {
299 if (!empty($dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) {
300 /** @var \Drupal\image\ImageStyleStorageInterface $storage */
301 $storage = \Drupal::entityManager()->getStorage($style->getEntityTypeId());
302 $replacement_id = $storage->getReplacementId($style_id);
303 // If a valid replacement has been provided in the storage, replace the
304 // preview image style with the replacement.
305 if ($replacement_id && ImageStyle::load($replacement_id)) {
306 $this->setSetting('preview_image_style', $replacement_id);
308 // If there's no replacement or the replacement is invalid, disable the
311 $this->setSetting('preview_image_style', '');
313 // Signal that the formatter plugin settings were updated.