6c3852be04b1059f5451bb49ca6a00f3a401505a
[yaffs-website] / web / modules / contrib / image_widget_crop / src / Element / ImageCrop.php
1 <?php
2
3 namespace Drupal\image_widget_crop\Element;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Core\Render\Element\FormElement;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\StringTranslation\TranslatableMarkup;
9 use Drupal\crop\Entity\Crop;
10 use Drupal\crop\Entity\CropType;
11 use Drupal\file\FileInterface;
12 use RecursiveArrayIterator;
13 use RecursiveIteratorIterator;
14 use Drupal\file_entity\Entity\FileEntity;
15
16 /**
17  * Provides a form element for crop.
18  *
19  * @FormElement("image_crop")
20  */
21 class ImageCrop extends FormElement {
22
23   /**
24    * {@inheritdoc}
25    */
26   public function getInfo() {
27     return [
28       '#process' => [
29         [static::class, 'processCrop'],
30       ],
31       '#file' => NULL,
32       '#crop_preview_image_style' => 'crop_thumbnail',
33       '#crop_type_list' => [],
34       '#crop_types_required' => [],
35       '#warn_multiple_usages' => FALSE,
36       '#show_default_crop' => TRUE,
37       '#show_crop_area' => FALSE,
38       '#attached' => [
39         'library' => 'image_widget_crop/cropper.integration',
40       ],
41       '#element_validate' => [[self::class, 'cropRequired']],
42       '#tree' => TRUE,
43     ];
44   }
45
46   /**
47    * {@inheritdoc}
48    */
49   public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
50     $return = [];
51     if ($input) {
52       return $input;
53     }
54     return $return;
55   }
56
57   /**
58    * Render API callback: Expands the image_crop element type.
59    *
60    * @param array $element
61    *   An associative array containing the properties and children of the
62    *   form actions container.
63    * @param \Drupal\Core\Form\FormStateInterface $form_state
64    *   The current state of the form.
65    * @param array $complete_form
66    *   The complete form structure.
67    *
68    * @return array
69    *   The processed element.
70    */
71   public static function processCrop(array &$element, FormStateInterface $form_state, array &$complete_form) {
72     /** @var \Drupal\file\Entity\File $file */
73     $file = $element['#file'];
74     if (!empty($file) && preg_match('/image/', $file->getMimeType())) {
75       /** @var \Drupal\Core\Image\Image $image */
76       $image = \Drupal::service('image.factory')->get($file->getFileUri());
77       if (!$image->isValid()) {
78         $element['message'] = [
79           '#type' => 'container',
80           '#markup' => t('The file "@file" is not valid on element @name.', [
81             '@file' => $file->getFileUri(),
82             '@name' => $element['#name'],
83           ]),
84           '#attributes' => [
85             'class' => ['messages messages--error'],
86           ],
87         ];
88         // Stop image_crop process and display error message.
89         return $element;
90       }
91
92       $crop_type_list = $element['#crop_type_list'];
93       // Display all crop types if none is selected.
94       if (empty($crop_type_list)) {
95         /** @var \Drupal\image_widget_crop\ImageWidgetCropInterface $iwc_manager */
96         $iwc_manager = \Drupal::service('image_widget_crop.manager');
97         $available_crop_types = $iwc_manager->getAvailableCropType(CropType::getCropTypeNames());
98         $crop_type_list = array_keys($available_crop_types);
99       }
100       $element['crop_wrapper'] = [
101         '#type' => 'details',
102         '#title' => t('Crop image'),
103         '#attributes' => [
104           'class' => ['image-data__crop-wrapper'],
105           'data-drupal-iwc' => 'wrapper',
106         ],
107         '#open' => $element['#show_crop_area'],
108         '#weight' => 100,
109       ];
110
111       if ($element['#warn_multiple_usages']) {
112         // Warn the user if the crop is used more than once.
113         $usage_counter = self::countFileUsages($file);
114         if ($usage_counter > 1) {
115           $element['crop_reuse'] = [
116             '#type' => 'container',
117             '#markup' => t('This crop definition affects more usages of this image'),
118             '#attributes' => [
119               'class' => ['messages messages--warning'],
120             ],
121             '#weight' => -10,
122           ];
123         }
124       }
125
126       // Ensure that the ID of an element is unique.
127       $list_id = \Drupal::service('uuid')->generate();
128
129       $element['crop_wrapper'][$list_id] = [
130         '#type' => 'vertical_tabs',
131         '#parents' => [$list_id],
132       ];
133
134       /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $crop_type_storage */
135       $crop_type_storage = \Drupal::entityTypeManager()
136         ->getStorage('crop_type');
137
138       /** @var \Drupal\crop\Entity\CropType[] $crop_types */
139       if ($crop_types = $crop_type_storage->loadMultiple($crop_type_list)) {
140         foreach ($crop_types as $type => $crop_type) {
141           $ratio = $crop_type->getAspectRatio() ?: 'NaN';
142           $element['crop_wrapper'][$type] = [
143             '#type' => 'details',
144             '#title' => $crop_type->label(),
145             '#group' => $list_id,
146             '#attributes' => [
147               'data-drupal-iwc' => 'type',
148               'data-drupal-iwc-id' => $type,
149               'data-drupal-iwc-ratio' => $ratio,
150               'data-drupal-iwc-required' => self::isRequiredType($element, $type),
151               'data-drupal-iwc-show-default-crop' => $element['#show_default_crop'] ? 'true' : 'false',
152               'data-drupal-iwc-soft-limit' => Json::encode($crop_type->getSoftLimit()),
153               'data-drupal-iwc-hard-limit' => Json::encode($crop_type->getHardLimit()),
154               'data-drupal-iwc-original-width' => ($file instanceof FileEntity) ? $file->getMetadata('width') : getimagesize($file->getFileUri())[0],
155               'data-drupal-iwc-original-height' => ($file instanceof FileEntity) ? $file->getMetadata('height') : getimagesize($file->getFileUri())[1],
156             ],
157           ];
158
159           // Generation of html List with image & crop information.
160           $element['crop_wrapper'][$type]['crop_container'] = [
161             '#id' => $type,
162             '#type' => 'container',
163             '#attributes' => ['class' => ['crop-preview-wrapper', $list_id]],
164             '#weight' => -10,
165           ];
166
167           $element['crop_wrapper'][$type]['crop_container']['image'] = [
168             '#theme' => 'image_style',
169             '#style_name' => $element['#crop_preview_image_style'],
170             '#attributes' => [
171               'class' => ['crop-preview-wrapper__preview-image'],
172               'data-drupal-iwc' => 'image',
173             ],
174             '#uri' => $file->getFileUri(),
175             '#weight' => -10,
176           ];
177
178           $element['crop_wrapper'][$type]['crop_container']['reset'] = [
179             '#type' => 'button',
180             '#value' => t('Reset crop'),
181             '#attributes' => [
182               'class' => ['crop-preview-wrapper__crop-reset'],
183               'data-drupal-iwc' => 'reset',
184             ],
185             '#weight' => -10,
186           ];
187
188           // Generation of html List with image & crop information.
189           $element['crop_wrapper'][$type]['crop_container']['values'] = [
190             '#type' => 'container',
191             '#attributes' => ['class' => ['crop-preview-wrapper__value']],
192             '#weight' => -9,
193           ];
194
195           // Element to track whether cropping is applied or not.
196           $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied'] = [
197             '#type' => 'hidden',
198             '#attributes' => [
199               'data-drupal-iwc-value' => 'applied',
200               'data-drupal-iwc-id' => $type,
201             ],
202             '#default_value' => 0,
203           ];
204           $edit = FALSE;
205           $properties = [];
206           $form_state_values = $form_state->getValue($element['#parents']);
207           // Check if form state has values.
208           if (self::hasCropValues($element, $type, $form_state)) {
209             $form_state_properties = $form_state_values['crop_wrapper'][$type]['crop_container']['values'];
210             // If crop is applied by the form state we keep it that way.
211             if ($form_state_properties['crop_applied'] == '1') {
212               $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied']['#default_value'] = 1;
213               $edit = TRUE;
214             }
215             $properties = $form_state_properties;
216           }
217
218           /** @var \Drupal\crop\CropInterface $crop */
219           $crop = Crop::findCrop($file->getFileUri(), $type);
220           if ($crop) {
221             $edit = TRUE;
222             /** @var \Drupal\image_widget_crop\ImageWidgetCropInterface $iwc_manager */
223             $iwc_manager = \Drupal::service('image_widget_crop.manager');
224             $original_properties = $iwc_manager->getCropProperties($crop);
225
226             // If form state values have the same values that were saved or if
227             // form state has no values yet and there are saved values then we
228             // use the saved values.
229             $properties = $original_properties == $properties || empty($properties) ? $original_properties : $properties;
230             $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied']['#default_value'] = 1;
231             // If the user edits an entity and while adding new images resets an
232             // saved crop we keep it reset.
233             if (isset($properties['crop_applied']) && $properties['crop_applied'] == '0') {
234               $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied']['#default_value'] = 0;
235             }
236           }
237           self::getCropFormElement($element, 'crop_container', $properties, $edit, $type);
238         }
239         // Stock Original File Values.
240         $element['file-uri'] = [
241           '#type' => 'value',
242           '#value' => $file->getFileUri(),
243         ];
244
245         $element['file-id'] = [
246           '#type' => 'value',
247           '#value' => $file->id(),
248         ];
249       }
250     }
251     return $element;
252   }
253
254   /**
255    * Check if given  $crop_type is required for current instance or not.
256    *
257    * @param array $element
258    *   All form elements.
259    * @param string $crop_type_id
260    *   The id of the current crop.
261    *
262    * @return string
263    *   Return string "1" if given crop is required or "0".
264    */
265   public static function isRequiredType(array $element, $crop_type_id) {
266     return (string) (static::hasCropRequired($element) && in_array($crop_type_id, $element['#crop_types_required']) ?: FALSE);
267   }
268
269   /**
270    * Counts how many times a file has been used.
271    *
272    * @param \Drupal\file\FileInterface $file
273    *   The file entity to check usages.
274    *
275    * @return int
276    *   Returns how many times the file has been used.
277    */
278   public static function countFileUsages(FileInterface $file) {
279     $counter = 0;
280     $file_usage = \Drupal::service('file.usage')->listUsage($file);
281     foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($file_usage)) as $usage) {
282       $counter += (int) $usage;
283     }
284     return $counter;
285   }
286
287   /**
288    * Inject crop elements into the form.
289    *
290    * @param array $element
291    *   All form elements.
292    * @param string $element_wrapper_name
293    *   Name of element contains all crop properties.
294    * @param array $original_properties
295    *   All properties calculate for apply to.
296    * @param bool $edit
297    *   Context of this form.
298    * @param string $crop_type_id
299    *   The id of the current crop.
300    *
301    * @return array|null
302    *   Populate all crop elements into the form.
303    */
304   public static function getCropFormElement(array &$element, $element_wrapper_name, array $original_properties, $edit, $crop_type_id) {
305     $crop_properties = self::getCropFormProperties($original_properties, $edit);
306
307     // Generate all coordinate elements into the form when process is active.
308     foreach ($crop_properties as $property => $value) {
309       $crop_element = &$element['crop_wrapper'][$crop_type_id][$element_wrapper_name]['values'][$property];
310       $value_property = self::getCropFormPropertyValue($element, $crop_type_id, $edit, $value['value'], $property);
311       $crop_element = [
312         '#type' => 'hidden',
313         '#attributes' => ['data-drupal-iwc-value' => $property],
314         '#crop_type' => $crop_type_id,
315         '#element_name' => $property,
316         '#default_value' => $value_property,
317       ];
318
319       if ($property == 'height' || $property == 'width') {
320         $crop_element['#element_validate'] = [
321           [
322             static::class,
323             'validateHardLimit',
324           ],
325         ];
326       }
327     }
328     return $element;
329   }
330
331   /**
332    * Update crop elements of crop into the form.
333    *
334    * @param array $original_properties
335    *   All properties calculate for apply to.
336    * @param bool $edit
337    *   Context of this form.
338    *
339    * @return array|null
340    *   Populate all crop elements into the form.
341    */
342   public static function getCropFormProperties(array $original_properties, $edit) {
343     $crop_elements = self::setCoordinatesElement();
344     if (!empty($original_properties) && $edit) {
345       foreach ($crop_elements as $properties => $value) {
346         $crop_elements[$properties]['value'] = $original_properties[$properties];
347       }
348     }
349     return $crop_elements;
350   }
351
352   /**
353    * Get default value of property elements.
354    *
355    * @param array $element
356    *   All form elements without crop properties.
357    * @param string $crop_type
358    *   The id of the current crop.
359    * @param bool $edit
360    *   Context of this form.
361    * @param int|null $value
362    *   The values calculated by ImageCrop::getCropFormProperties().
363    * @param string $property
364    *   Name of current property @see setCoordinatesElement().
365    *
366    * @return int|null
367    *   Value of this element.
368    */
369   public static function getCropFormPropertyValue(array &$element, $crop_type, $edit, $value, $property) {
370     // Standard case.
371     if (!empty($edit) && isset($value)) {
372       return $value;
373     }
374     // Populate value when ajax populates values after process.
375     if (isset($element['#value']) && isset($element['crop_wrapper'])) {
376       $ajax_element = &$element['#value']['crop_wrapper']['container'][$crop_type]['values'];
377       return (isset($ajax_element[$property]) && !empty($ajax_element[$property])) ? $ajax_element[$property] : NULL;
378     }
379     return NULL;
380   }
381
382   /**
383    * Form element validation handler for crop widget elements.
384    *
385    * @param array $element
386    *   All form elements without crop properties.
387    * @param \Drupal\Core\Form\FormStateInterface $form_state
388    *   The current state of the form.
389    *
390    * @see ImageCrop::getCropFormElement()
391    */
392   public static function validateHardLimit(array $element, FormStateInterface $form_state) {
393     /** @var \Drupal\crop\Entity\CropType $crop_type */
394     $crop_type = \Drupal::entityTypeManager()
395       ->getStorage('crop_type')
396       ->load($element['#crop_type']);
397     $parents = $element['#parents'];
398     array_pop($parents);
399     $crop_values = $form_state->getValue($parents);
400     $hard_limit = $crop_type->getHardLimit();
401     $action_button = $form_state->getTriggeringElement()['#value'];
402     // We need to add this test in multilingual context because,
403     // the "#value" element are a simple string in translate form,
404     // and an TranslatableMarkup object in other cases.
405     $operation = ($action_button instanceof TranslatableMarkup) ? $action_button->getUntranslatedString() : $action_button;
406
407     if ((int) $crop_values['crop_applied'] == 0 || $operation == 'Remove') {
408       return;
409     }
410
411     $element_name = $element['#element_name'];
412     if ($hard_limit[$element_name] !== 0 && !empty($hard_limit[$element_name])) {
413       if ($hard_limit[$element_name] > (int) $crop_values[$element_name]) {
414         $form_state->setError($element, t('Crop @property is smaller than the allowed @hard_limitpx for @crop_name',
415           [
416             '@property' => $element_name,
417             '@hard_limit' => $hard_limit[$element_name],
418             '@crop_name' => $crop_type->label(),
419           ]
420         ));
421       }
422     }
423   }
424
425   /**
426    * Evaluate if current element has required crops set from widget settings.
427    *
428    * @param array $element
429    *   All form elements without crop properties.
430    *
431    * @return bool
432    *   True if 'crop_types_required' settings is set or False.
433    *
434    * @see ImageCrop::cropRequired()
435    */
436   public static function hasCropRequired(array $element) {
437     return isset($element['#crop_types_required']) || !empty($element['#crop_types_required']);
438   }
439
440   /**
441    * Form element validation handler for crop widget elements.
442    *
443    * @param array $element
444    *   All form elements without crop properties.
445    * @param \Drupal\Core\Form\FormStateInterface $form_state
446    *   The current state of the form.
447    *
448    * @see ImageCrop::getCropFormElement()
449    */
450   public static function cropRequired(array $element, FormStateInterface $form_state) {
451     if (!static::hasCropRequired($element)) {
452       return;
453     }
454
455     $required_crops = [];
456     foreach ($element['#crop_types_required'] as $crop_type_id) {
457       $crop_applied = $form_state->getValue($element['#parents'])['crop_wrapper'][$crop_type_id]['crop_container']['values']['crop_applied'];
458       $action_button = $form_state->getTriggeringElement()['#value'];
459       $operation = ($action_button instanceof TranslatableMarkup) ? $action_button->getUntranslatedString() : $action_button;
460
461       if (self::fileTriggered($form_state) && self::requiredApplicable($crop_applied, $operation)) {
462         /** @var \Drupal\crop\Entity\CropType $crop_type */
463         $crop_type = \Drupal::entityTypeManager()
464           ->getStorage('crop_type')
465           ->load($crop_type_id);
466         $required_crops[] = $crop_type->label();
467       }
468     }
469
470     if (!empty($required_crops)) {
471       $form_state->setError($element, \Drupal::translation()
472         ->formatPlural(count($required_crops), '@crop_required is required.', '@crops_required are required.', [
473           "@crop_required" => current($required_crops),
474           "@crops_required" => implode(', ', $required_crops),
475         ]
476         ));
477     }
478   }
479
480   /**
481    * Unsure we have triggered 'file_managed_file_submit' button.
482    *
483    * @param \Drupal\Core\Form\FormStateInterface $form_state
484    *   The current state of the form.
485    *
486    * @return bool
487    *   True if triggered button are 'file_managed_file_submit' or False.
488    */
489   public static function fileTriggered(FormStateInterface $form_state) {
490     return !in_array('file_managed_file_submit', $form_state->getTriggeringElement()['#submit']);
491   }
492
493   /**
494    * Evaluate if crop is applicable on current CropType.
495    *
496    * @param int $crop_applied
497    *   Crop applied parents.
498    * @param string $operation
499    *   Label current operation.
500    *
501    * @return bool
502    *   True if current crop operation isn't "Reset crop" or False.
503    */
504   public static function requiredApplicable($crop_applied, $operation) {
505     return ((int) $crop_applied === 0 && $operation !== 'Remove');
506   }
507
508   /**
509    * Set All sizes properties of the crops.
510    *
511    * @return array|null
512    *   Set all possible crop zone properties.
513    */
514   public static function setCoordinatesElement() {
515     return [
516       'x' => ['label' => t('X coordinate'), 'value' => NULL],
517       'y' => ['label' => t('Y coordinate'), 'value' => NULL],
518       'width' => ['label' => t('Width'), 'value' => NULL],
519       'height' => ['label' => t('Height'), 'value' => NULL],
520     ];
521   }
522
523   /**
524    * Evaluate if element has crop values in form states.
525    *
526    * @param array $element
527    *   An associative array containing the properties and children of the
528    *   form actions container.
529    * @param string $type
530    *   Id of current crop type.
531    * @param \Drupal\Core\Form\FormStateInterface $form_state
532    *   The current state of the form.
533    *
534    * @return bool
535    *   True if crop element have values or False if not.
536    */
537   public static function hasCropValues(array $element, $type, FormStateInterface $form_state) {
538     $form_state_values = $form_state->getValue($element['#parents']);
539     return !empty($form_state_values) && isset($form_state_values['crop_wrapper'][$type]);
540   }
541
542 }