3 namespace Drupal\image_widget_crop\Element;
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;
17 * Provides a form element for crop.
19 * @FormElement("image_crop")
21 class ImageCrop extends FormElement {
26 public function getInfo() {
29 [static::class, 'processCrop'],
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,
39 'library' => 'image_widget_crop/cropper.integration',
41 '#element_validate' => [[self::class, 'cropRequired']],
49 public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
58 * Render API callback: Expands the image_crop element type.
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.
69 * The processed element.
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'],
85 'class' => ['messages messages--error'],
88 // Stop image_crop process and display error message.
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);
100 $element['crop_wrapper'] = [
101 '#type' => 'details',
102 '#title' => t('Crop image'),
104 'class' => ['image-data__crop-wrapper'],
105 'data-drupal-iwc' => 'wrapper',
107 '#open' => $element['#show_crop_area'],
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'),
119 'class' => ['messages messages--warning'],
126 // Ensure that the ID of an element is unique.
127 $list_id = \Drupal::service('uuid')->generate();
129 $element['crop_wrapper'][$list_id] = [
130 '#type' => 'vertical_tabs',
131 '#parents' => [$list_id],
134 /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $crop_type_storage */
135 $crop_type_storage = \Drupal::entityTypeManager()
136 ->getStorage('crop_type');
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,
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],
159 // Generation of html List with image & crop information.
160 $element['crop_wrapper'][$type]['crop_container'] = [
162 '#type' => 'container',
163 '#attributes' => ['class' => ['crop-preview-wrapper', $list_id]],
167 $element['crop_wrapper'][$type]['crop_container']['image'] = [
168 '#theme' => 'image_style',
169 '#style_name' => $element['#crop_preview_image_style'],
171 'class' => ['crop-preview-wrapper__preview-image'],
172 'data-drupal-iwc' => 'image',
174 '#uri' => $file->getFileUri(),
178 $element['crop_wrapper'][$type]['crop_container']['reset'] = [
180 '#value' => t('Reset crop'),
182 'class' => ['crop-preview-wrapper__crop-reset'],
183 'data-drupal-iwc' => 'reset',
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']],
195 // Element to track whether cropping is applied or not.
196 $element['crop_wrapper'][$type]['crop_container']['values']['crop_applied'] = [
199 'data-drupal-iwc-value' => 'applied',
200 'data-drupal-iwc-id' => $type,
202 '#default_value' => 0,
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;
215 $properties = $form_state_properties;
218 /** @var \Drupal\crop\CropInterface $crop */
219 $crop = Crop::findCrop($file->getFileUri(), $type);
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);
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;
237 self::getCropFormElement($element, 'crop_container', $properties, $edit, $type);
239 // Stock Original File Values.
240 $element['file-uri'] = [
242 '#value' => $file->getFileUri(),
245 $element['file-id'] = [
247 '#value' => $file->id(),
255 * Check if given $crop_type is required for current instance or not.
257 * @param array $element
259 * @param string $crop_type_id
260 * The id of the current crop.
263 * Return string "1" if given crop is required or "0".
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);
270 * Counts how many times a file has been used.
272 * @param \Drupal\file\FileInterface $file
273 * The file entity to check usages.
276 * Returns how many times the file has been used.
278 public static function countFileUsages(FileInterface $file) {
280 $file_usage = \Drupal::service('file.usage')->listUsage($file);
281 foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($file_usage)) as $usage) {
282 $counter += (int) $usage;
288 * Inject crop elements into the form.
290 * @param array $element
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.
297 * Context of this form.
298 * @param string $crop_type_id
299 * The id of the current crop.
302 * Populate all crop elements into the form.
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);
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);
313 '#attributes' => ['data-drupal-iwc-value' => $property],
314 '#crop_type' => $crop_type_id,
315 '#element_name' => $property,
316 '#default_value' => $value_property,
319 if ($property == 'height' || $property == 'width') {
320 $crop_element['#element_validate'] = [
332 * Update crop elements of crop into the form.
334 * @param array $original_properties
335 * All properties calculate for apply to.
337 * Context of this form.
340 * Populate all crop elements into the form.
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];
349 return $crop_elements;
353 * Get default value of property elements.
355 * @param array $element
356 * All form elements without crop properties.
357 * @param string $crop_type
358 * The id of the current crop.
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().
367 * Value of this element.
369 public static function getCropFormPropertyValue(array &$element, $crop_type, $edit, $value, $property) {
371 if (!empty($edit) && isset($value)) {
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;
383 * Form element validation handler for crop widget elements.
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.
390 * @see ImageCrop::getCropFormElement()
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'];
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;
407 if ((int) $crop_values['crop_applied'] == 0 || $operation == 'Remove') {
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',
416 '@property' => $element_name,
417 '@hard_limit' => $hard_limit[$element_name],
418 '@crop_name' => $crop_type->label(),
426 * Evaluate if current element has required crops set from widget settings.
428 * @param array $element
429 * All form elements without crop properties.
432 * True if 'crop_types_required' settings is set or False.
434 * @see ImageCrop::cropRequired()
436 public static function hasCropRequired(array $element) {
437 return isset($element['#crop_types_required']) || !empty($element['#crop_types_required']);
441 * Form element validation handler for crop widget elements.
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.
448 * @see ImageCrop::getCropFormElement()
450 public static function cropRequired(array $element, FormStateInterface $form_state) {
451 if (!static::hasCropRequired($element)) {
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;
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();
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),
481 * Unsure we have triggered 'file_managed_file_submit' button.
483 * @param \Drupal\Core\Form\FormStateInterface $form_state
484 * The current state of the form.
487 * True if triggered button are 'file_managed_file_submit' or False.
489 public static function fileTriggered(FormStateInterface $form_state) {
490 return !in_array('file_managed_file_submit', $form_state->getTriggeringElement()['#submit']);
494 * Evaluate if crop is applicable on current CropType.
496 * @param int $crop_applied
497 * Crop applied parents.
498 * @param string $operation
499 * Label current operation.
502 * True if current crop operation isn't "Reset crop" or False.
504 public static function requiredApplicable($crop_applied, $operation) {
505 return ((int) $crop_applied === 0 && $operation !== 'Remove');
509 * Set All sizes properties of the crops.
512 * Set all possible crop zone properties.
514 public static function setCoordinatesElement() {
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],
524 * Evaluate if element has crop values in form states.
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.
535 * True if crop element have values or False if not.
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]);