a83ec0defb90300a5d28ba8c8f71061afcf19f2a
[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\Core\Render\Element\FormElement;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\Core\StringTranslation\TranslatableMarkup;
8 use Drupal\crop\Entity\Crop;
9 use Drupal\crop\Entity\CropType;
10 use Drupal\file\FileInterface;
11 use RecursiveArrayIterator;
12 use RecursiveIteratorIterator;
13 use Drupal\file_entity\Entity\FileEntity;
14
15 /**
16  * Provides a form element for crop.
17  *
18  * @FormElement("image_crop")
19  */
20 class ImageCrop extends FormElement {
21
22   /**
23    * {@inheritdoc}
24    */
25   public function getInfo() {
26     return [
27       '#process' => [
28         [static::class, 'processCrop'],
29       ],
30       '#file' => NULL,
31       '#crop_preview_image_style' => 'crop_thumbnail',
32       '#crop_type_list' => [],
33       '#warn_multiple_usages' => FALSE,
34       '#show_default_crop' => TRUE,
35       '#show_crop_area' => FALSE,
36       '#attached' => [
37         'library' => 'image_widget_crop/cropper.integration',
38       ],
39       '#tree' => TRUE,
40     ];
41   }
42
43   /**
44    * {@inheritdoc}
45    */
46   public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
47     $return = [];
48     if ($input) {
49       return $input;
50     }
51     return $return;
52   }
53
54   /**
55    * Render API callback: Expands the image_crop element type.
56    */
57   public static function processCrop(&$element, FormStateInterface $form_state, &$complete_form) {
58     /** @var \Drupal\file\Entity\File $file */
59     $file = $element['#file'];
60     if (!empty($file) && preg_match('/image/', $file->getMimeType())) {
61       $element['#attached']['drupalSettings']['crop_default'] = $element['#show_default_crop'];
62
63       /** @var \Drupal\Core\Image\Image $image */
64       $image = \Drupal::service('image.factory')->get($file->getFileUri());
65       if (!$image->isValid()) {
66         $element['message'] = [
67           '#type' => 'container',
68           '#markup' => t('The file "@file" is not valid on element @name.', [
69             '@file' => $file->getFileUri(),
70             '@name' => $element['#name'],
71           ]),
72           '#attributes' => [
73             'class' => ['messages messages--error'],
74           ],
75         ];
76         // Stop image_crop process and display error message.
77         return $element;
78       }
79
80       $crop_type_list = $element['#crop_type_list'];
81       // Display all crop types if none is selected.
82       if (empty($crop_type_list)) {
83         /** @var \Drupal\image_widget_crop\ImageWidgetCropManager $image_widget_crop_manager */
84         $image_widget_crop_manager = \Drupal::service('image_widget_crop.manager');
85         $available_crop_types = $image_widget_crop_manager->getAvailableCropType(CropType::getCropTypeNames());
86         $crop_type_list = array_keys($available_crop_types);
87       }
88       $element['crop_wrapper'] = [
89         '#type' => 'details',
90         '#title' => t('Crop image'),
91         '#attributes' => ['class' => ['image-data__crop-wrapper']],
92         '#open' => $element['#show_crop_area'],
93         '#weight' => 100,
94       ];
95
96       if ($element['#warn_multiple_usages']) {
97         // Warn the user if the crop is used more than once.
98         $usage_counter = self::countFileUsages($file);
99         if ($usage_counter > 1) {
100           $element['crop_reuse'] = [
101             '#type' => 'container',
102             '#markup' => t('This crop definition affects more usages of this image'),
103             '#attributes' => [
104               'class' => ['messages messages--warning'],
105             ],
106             '#weight' => -10,
107           ];
108         }
109       }
110
111       // Ensure that the ID of an element is unique.
112       $list_id = \Drupal::service('uuid')->generate();
113
114       $element['crop_wrapper'][$list_id] = [
115         '#type' => 'vertical_tabs',
116         '#theme_wrappers' => ['vertical_tabs'],
117         '#parents' => [$list_id],
118       ];
119
120       /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $crop_type_storage */
121       $crop_type_storage = \Drupal::entityTypeManager()->getStorage('crop_type');
122       if (!empty($crop_type_storage->loadMultiple())) {
123         foreach ($crop_type_list as $crop_type) {
124           /** @var \Drupal\crop\Entity\CropType $crop_type */
125           $crop_type = $crop_type_storage->load($crop_type);
126           $ratio = $crop_type->getAspectRatio() ? $crop_type->getAspectRatio() : 'Nan';
127
128           $element['#attached']['drupalSettings']['image_widget_crop'][$crop_type->id()] = [
129             'soft_limit' => $crop_type->getSoftLimit(),
130             'hard_limit' => $crop_type->getHardLimit(),
131           ];
132
133           $element['crop_wrapper'][$crop_type->id()] = [
134             '#type' => 'details',
135             '#title' => $crop_type->label(),
136             '#group' => $list_id,
137           ];
138
139           // Generation of html List with image & crop information.
140           $element['crop_wrapper'][$crop_type->id()]['crop_container'] = [
141             '#type' => 'container',
142             '#attributes' => [
143               'class' => ['crop-preview-wrapper', $list_id],
144               'id' => [$crop_type->id()],
145               'data-ratio' => [$ratio],
146             ],
147             '#weight' => -10,
148           ];
149
150           $element['crop_wrapper'][$crop_type->id()]['crop_container']['image'] = [
151             '#theme' => 'image_style',
152             '#style_name' => $element['#crop_preview_image_style'],
153             '#attributes' => [
154               'class' => ['crop-preview-wrapper__preview-image'],
155               'data-ratio' => $ratio,
156               'data-name' => $crop_type->id(),
157               'data-original-width' => ($file instanceof FileEntity) ? $file->getMetadata('width') : getimagesize($file->getFileUri())[0],
158               'data-original-height' => ($file instanceof FileEntity) ? $file->getMetadata('height') : getimagesize($file->getFileUri())[1],
159             ],
160             '#uri' => $file->getFileUri(),
161             '#weight' => -10,
162           ];
163
164           $element['crop_wrapper'][$crop_type->id()]['crop_container']['reset'] = [
165             '#type' => 'button',
166             '#value' => t('Reset crop'),
167             '#attributes' => ['class' => ['crop-preview-wrapper__crop-reset']],
168             '#weight' => -10,
169           ];
170
171           // Generation of html List with image & crop information.
172           $element['crop_wrapper'][$crop_type->id()]['crop_container']['values'] = [
173             '#type' => 'container',
174             '#attributes' => ['class' => ['crop-preview-wrapper__value']],
175             '#weight' => -9,
176           ];
177
178           // Element to track whether cropping is applied or not.
179           $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied'] = [
180             '#type' => 'hidden',
181             '#attributes' => ['class' => ["crop-applied"]],
182             '#default_value' => 0,
183           ];
184           $edit = FALSE;
185           $properties = [];
186           $form_state_element_values = $form_state->getValue($element['#parents']);
187           // Check if form state has values.
188           if ($form_state_element_values) {
189             $form_state_properties = $form_state_element_values['crop_wrapper'][$crop_type->id()]['crop_container']['values'];
190             // If crop is applied by the form state we keep it that way.
191             if ($form_state_properties['crop_applied'] == '1') {
192               $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 1;
193               $edit = TRUE;
194             }
195             $properties = $form_state_properties;
196           }
197
198           /** @var \Drupal\crop\Entity\Crop $crop */
199           $crop = Crop::findCrop($file->getFileUri(), $crop_type->id());
200           if ($crop) {
201             $edit = TRUE;
202             /** @var \Drupal\image_widget_crop\ImageWidgetCropManager $image_widget_crop_manager */
203             $image_widget_crop_manager = \Drupal::service('image_widget_crop.manager');
204             $original_properties = $image_widget_crop_manager->getCropProperties($crop);
205
206             // If form state values have the same values that were saved or if
207             // form state has no values yet and there are saved values then we
208             // use the saved values.
209             $properties = $original_properties == $properties || empty($properties) ? $original_properties : $properties;
210             $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 1;
211             // If the user edits an entity and while adding new images resets an
212             // saved crop we keep it reset.
213             if (isset($properties['crop_applied']) && $properties['crop_applied'] == '0') {
214               $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 0;
215             }
216           }
217           self::getCropFormElement($element, 'crop_container', $properties, $edit, $crop_type->id());
218         }
219         // Stock Original File Values.
220         $element['file-uri'] = [
221           '#type' => 'value',
222           '#value' => $file->getFileUri(),
223         ];
224
225         $element['file-id'] = [
226           '#type' => 'value',
227           '#value' => $file->id(),
228         ];
229       }
230     }
231     return $element;
232   }
233
234   /**
235    * Counts how many times a file has been used.
236    *
237    * @param \Drupal\file\FileInterface $file
238    *   The file entity to check usages.
239    *
240    * @return int
241    *   Returns how many times the file has been used.
242    */
243   public static function countFileUsages(FileInterface $file) {
244     $counter = 0;
245     $file_usage = \Drupal::service('file.usage')->listUsage($file);
246     foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($file_usage)) as $usage) {
247       $counter += (int) $usage;
248     }
249     return $counter;
250   }
251
252   /**
253    * Inject crop elements into the form.
254    *
255    * @param array $element
256    *   All form elements.
257    * @param string $element_wrapper_name
258    *   Name of element contains all crop properties.
259    * @param array $original_properties
260    *   All properties calculate for apply to.
261    * @param bool $edit
262    *   Context of this form.
263    * @param string $crop_type_id
264    *   The id of the current crop.
265    *
266    * @return array|null
267    *   Populate all crop elements into the form.
268    */
269   public static function getCropFormElement(array &$element, $element_wrapper_name, array $original_properties, $edit, $crop_type_id) {
270     $crop_properties = self::getCropFormProperties($original_properties, $edit);
271
272     // Generate all coordinate elements into the form when process is active.
273     foreach ($crop_properties as $property => $value) {
274       $crop_element = &$element['crop_wrapper'][$crop_type_id][$element_wrapper_name]['values'][$property];
275       $value_property = self::getCropFormPropertyValue($element, $crop_type_id, $edit, $value['value'], $property);
276       $crop_element = [
277         '#type' => 'hidden',
278         '#attributes' => [
279           'class' => ["crop-$property"],
280         ],
281         '#crop_type' => $crop_type_id,
282         '#element_name' => $property,
283         '#default_value' => $value_property,
284       ];
285
286       if ($property == 'height' || $property == 'width') {
287         $crop_element['#element_validate'] = [
288           [
289             static::class,
290             'validateHardLimit',
291           ],
292         ];
293       }
294     }
295     return $element;
296   }
297
298   /**
299    * Update crop elements of crop into the form.
300    *
301    * @param array $original_properties
302    *   All properties calculate for apply to.
303    * @param bool $edit
304    *   Context of this form.
305    *
306    * @return array<string,array>
307    *   Populate all crop elements into the form.
308    */
309   public static function getCropFormProperties(array $original_properties, $edit) {
310     $crop_elements = self::setCoordinatesElement();
311     if (!empty($original_properties) && $edit) {
312       foreach ($crop_elements as $properties => $value) {
313         $crop_elements[$properties]['value'] = $original_properties[$properties];
314       }
315     }
316     return $crop_elements;
317   }
318
319   /**
320    * Get default value of property elements.
321    *
322    * @param array $element
323    *   All form elements without crop properties.
324    * @param string $crop_type
325    *   The id of the current crop.
326    * @param bool $edit
327    *   Context of this form.
328    * @param int|null $value
329    *   The values calculated by ImageCrop::getCropFormProperties().
330    * @param string $property
331    *   Name of current property @see setCoordinatesElement().
332    *
333    * @return int|null
334    *   Value of this element.
335    */
336   public static function getCropFormPropertyValue(array &$element, $crop_type, $edit, $value, $property) {
337     // Standard case.
338     if (!empty($edit) && isset($value)) {
339       return $value;
340     }
341     // Populate value when ajax populates values after process.
342     if (isset($element['#value']) && isset($element['crop_wrapper'])) {
343       $ajax_element = &$element['#value']['crop_wrapper']['container'][$crop_type]['values'];
344       return (isset($ajax_element[$property]) && !empty($ajax_element[$property])) ? $ajax_element[$property] : NULL;
345     }
346     return NULL;
347   }
348
349   /**
350    * Form element validation handler for crop widget elements.
351    *
352    * @param array $element
353    *   All form elements without crop properties.
354    * @param \Drupal\Core\Form\FormStateInterface $form_state
355    *   The current state of the form.
356    *
357    * @see ImageCrop::getCropFormElement()
358    */
359   public static function validateHardLimit(array $element, FormStateInterface $form_state) {
360     /** @var \Drupal\crop\Entity\CropType $crop_type */
361     $crop_type = \Drupal::entityTypeManager()
362       ->getStorage('crop_type')
363       ->load($element['#crop_type']);
364     $parents = $element['#parents'];
365     array_pop($parents);
366     $crop_values = $form_state->getValue($parents);
367     $hard_limit = $crop_type->getHardLimit();
368     $action_button = $form_state->getTriggeringElement()['#value'];
369     // @todo We need to add this test in multilingual context because,
370     // the "#value" element are a simple string in translate form,
371     // and an TranslatableMarkup object in other cases.
372     $operation = ($action_button instanceof TranslatableMarkup) ? $action_button->getUntranslatedString() : $action_button;
373
374     if ((int) $crop_values['crop_applied'] == 0 || $operation == 'Remove') {
375       return;
376     }
377
378     $element_name = $element['#element_name'];
379     if ($hard_limit[$element_name] !== 0 && !empty($hard_limit[$element_name])) {
380       if ($hard_limit[$element_name] > (int) $crop_values[$element_name]) {
381         $form_state->setError($element, t('Crop @property is smaller then the allowed @hard_limitpx for @crop_name',
382           [
383             '@property' => $element_name,
384             '@hard_limit' => $hard_limit[$element_name],
385             '@crop_name' => $crop_type->label(),
386           ]
387           ));
388       }
389     }
390   }
391
392   /**
393    * Set All sizes properties of the crops.
394    *
395    * @return array<string,array>
396    *   Set all possible crop zone properties.
397    */
398   public static function setCoordinatesElement() {
399     return [
400       'x' => ['label' => t('X coordinate'), 'value' => NULL],
401       'y' => ['label' => t('Y coordinate'), 'value' => NULL],
402       'width' => ['label' => t('Width'), 'value' => NULL],
403       'height' => ['label' => t('Height'), 'value' => NULL],
404     ];
405   }
406
407 }