--- /dev/null
+<?php
+
+namespace Drupal\image_widget_crop\Element;
+
+use Drupal\Core\Render\Element\FormElement;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\crop\Entity\Crop;
+use Drupal\crop\Entity\CropType;
+use Drupal\file\FileInterface;
+use RecursiveArrayIterator;
+use RecursiveIteratorIterator;
+use Drupal\file_entity\Entity\FileEntity;
+
+/**
+ * Provides a form element for crop.
+ *
+ * @FormElement("image_crop")
+ */
+class ImageCrop extends FormElement {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo() {
+ return [
+ '#process' => [
+ [static::class, 'processCrop'],
+ ],
+ '#file' => NULL,
+ '#crop_preview_image_style' => 'crop_thumbnail',
+ '#crop_type_list' => [],
+ '#warn_multiple_usages' => FALSE,
+ '#show_default_crop' => TRUE,
+ '#show_crop_area' => FALSE,
+ '#attached' => [
+ 'library' => 'image_widget_crop/cropper.integration',
+ ],
+ '#tree' => TRUE,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+ $return = [];
+ if ($input) {
+ return $input;
+ }
+ return $return;
+ }
+
+ /**
+ * Render API callback: Expands the image_crop element type.
+ */
+ public static function processCrop(&$element, FormStateInterface $form_state, &$complete_form) {
+ /** @var \Drupal\file\Entity\File $file */
+ $file = $element['#file'];
+ if (!empty($file) && preg_match('/image/', $file->getMimeType())) {
+ $element['#attached']['drupalSettings']['crop_default'] = $element['#show_default_crop'];
+
+ /** @var \Drupal\Core\Image\Image $image */
+ $image = \Drupal::service('image.factory')->get($file->getFileUri());
+ if (!$image->isValid()) {
+ $element['message'] = [
+ '#type' => 'container',
+ '#markup' => t('The file "@file" is not valid on element @name.', [
+ '@file' => $file->getFileUri(),
+ '@name' => $element['#name'],
+ ]),
+ '#attributes' => [
+ 'class' => ['messages messages--error'],
+ ],
+ ];
+ // Stop image_crop process and display error message.
+ return $element;
+ }
+
+ $crop_type_list = $element['#crop_type_list'];
+ // Display all crop types if none is selected.
+ if (empty($crop_type_list)) {
+ /** @var \Drupal\image_widget_crop\ImageWidgetCropManager $image_widget_crop_manager */
+ $image_widget_crop_manager = \Drupal::service('image_widget_crop.manager');
+ $available_crop_types = $image_widget_crop_manager->getAvailableCropType(CropType::getCropTypeNames());
+ $crop_type_list = array_keys($available_crop_types);
+ }
+ $element['crop_wrapper'] = [
+ '#type' => 'details',
+ '#title' => t('Crop image'),
+ '#attributes' => ['class' => ['image-data__crop-wrapper']],
+ '#open' => $element['#show_crop_area'],
+ '#weight' => 100,
+ ];
+
+ if ($element['#warn_multiple_usages']) {
+ // Warn the user if the crop is used more than once.
+ $usage_counter = self::countFileUsages($file);
+ if ($usage_counter > 1) {
+ $element['crop_reuse'] = [
+ '#type' => 'container',
+ '#markup' => t('This crop definition affects more usages of this image'),
+ '#attributes' => [
+ 'class' => ['messages messages--warning'],
+ ],
+ '#weight' => -10,
+ ];
+ }
+ }
+
+ // Ensure that the ID of an element is unique.
+ $list_id = \Drupal::service('uuid')->generate();
+
+ $element['crop_wrapper'][$list_id] = [
+ '#type' => 'vertical_tabs',
+ '#theme_wrappers' => ['vertical_tabs'],
+ '#parents' => [$list_id],
+ ];
+
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $crop_type_storage */
+ $crop_type_storage = \Drupal::entityTypeManager()->getStorage('crop_type');
+ if (!empty($crop_type_storage->loadMultiple())) {
+ foreach ($crop_type_list as $crop_type) {
+ /** @var \Drupal\crop\Entity\CropType $crop_type */
+ $crop_type = $crop_type_storage->load($crop_type);
+ $ratio = $crop_type->getAspectRatio() ? $crop_type->getAspectRatio() : 'Nan';
+
+ $element['#attached']['drupalSettings']['image_widget_crop'][$crop_type->id()] = [
+ 'soft_limit' => $crop_type->getSoftLimit(),
+ 'hard_limit' => $crop_type->getHardLimit(),
+ ];
+
+ $element['crop_wrapper'][$crop_type->id()] = [
+ '#type' => 'details',
+ '#title' => $crop_type->label(),
+ '#group' => $list_id,
+ ];
+
+ // Generation of html List with image & crop information.
+ $element['crop_wrapper'][$crop_type->id()]['crop_container'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => ['crop-preview-wrapper', $list_id],
+ 'id' => [$crop_type->id()],
+ 'data-ratio' => [$ratio],
+ ],
+ '#weight' => -10,
+ ];
+
+ $element['crop_wrapper'][$crop_type->id()]['crop_container']['image'] = [
+ '#theme' => 'image_style',
+ '#style_name' => $element['#crop_preview_image_style'],
+ '#attributes' => [
+ 'class' => ['crop-preview-wrapper__preview-image'],
+ 'data-ratio' => $ratio,
+ 'data-name' => $crop_type->id(),
+ 'data-original-width' => ($file instanceof FileEntity) ? $file->getMetadata('width') : getimagesize($file->getFileUri())[0],
+ 'data-original-height' => ($file instanceof FileEntity) ? $file->getMetadata('height') : getimagesize($file->getFileUri())[1],
+ ],
+ '#uri' => $file->getFileUri(),
+ '#weight' => -10,
+ ];
+
+ $element['crop_wrapper'][$crop_type->id()]['crop_container']['reset'] = [
+ '#type' => 'button',
+ '#value' => t('Reset crop'),
+ '#attributes' => ['class' => ['crop-preview-wrapper__crop-reset']],
+ '#weight' => -10,
+ ];
+
+ // Generation of html List with image & crop information.
+ $element['crop_wrapper'][$crop_type->id()]['crop_container']['values'] = [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['crop-preview-wrapper__value']],
+ '#weight' => -9,
+ ];
+
+ // Element to track whether cropping is applied or not.
+ $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied'] = [
+ '#type' => 'hidden',
+ '#attributes' => ['class' => ["crop-applied"]],
+ '#default_value' => 0,
+ ];
+ $edit = FALSE;
+ $properties = [];
+ $form_state_element_values = $form_state->getValue($element['#parents']);
+ // Check if form state has values.
+ if ($form_state_element_values) {
+ $form_state_properties = $form_state_element_values['crop_wrapper'][$crop_type->id()]['crop_container']['values'];
+ // If crop is applied by the form state we keep it that way.
+ if ($form_state_properties['crop_applied'] == '1') {
+ $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 1;
+ $edit = TRUE;
+ }
+ $properties = $form_state_properties;
+ }
+
+ /** @var \Drupal\crop\Entity\Crop $crop */
+ $crop = Crop::findCrop($file->getFileUri(), $crop_type->id());
+ if ($crop) {
+ $edit = TRUE;
+ /** @var \Drupal\image_widget_crop\ImageWidgetCropManager $image_widget_crop_manager */
+ $image_widget_crop_manager = \Drupal::service('image_widget_crop.manager');
+ $original_properties = $image_widget_crop_manager->getCropProperties($crop);
+
+ // If form state values have the same values that were saved or if
+ // form state has no values yet and there are saved values then we
+ // use the saved values.
+ $properties = $original_properties == $properties || empty($properties) ? $original_properties : $properties;
+ $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 1;
+ // If the user edits an entity and while adding new images resets an
+ // saved crop we keep it reset.
+ if (isset($properties['crop_applied']) && $properties['crop_applied'] == '0') {
+ $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied']['#default_value'] = 0;
+ }
+ }
+ self::getCropFormElement($element, 'crop_container', $properties, $edit, $crop_type->id());
+ }
+ // Stock Original File Values.
+ $element['file-uri'] = [
+ '#type' => 'value',
+ '#value' => $file->getFileUri(),
+ ];
+
+ $element['file-id'] = [
+ '#type' => 'value',
+ '#value' => $file->id(),
+ ];
+ }
+ }
+ return $element;
+ }
+
+ /**
+ * Counts how many times a file has been used.
+ *
+ * @param \Drupal\file\FileInterface $file
+ * The file entity to check usages.
+ *
+ * @return int
+ * Returns how many times the file has been used.
+ */
+ public static function countFileUsages(FileInterface $file) {
+ $counter = 0;
+ $file_usage = \Drupal::service('file.usage')->listUsage($file);
+ foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($file_usage)) as $usage) {
+ $counter += (int) $usage;
+ }
+ return $counter;
+ }
+
+ /**
+ * Inject crop elements into the form.
+ *
+ * @param array $element
+ * All form elements.
+ * @param string $element_wrapper_name
+ * Name of element contains all crop properties.
+ * @param array $original_properties
+ * All properties calculate for apply to.
+ * @param bool $edit
+ * Context of this form.
+ * @param string $crop_type_id
+ * The id of the current crop.
+ *
+ * @return array|null
+ * Populate all crop elements into the form.
+ */
+ public static function getCropFormElement(array &$element, $element_wrapper_name, array $original_properties, $edit, $crop_type_id) {
+ $crop_properties = self::getCropFormProperties($original_properties, $edit);
+
+ // Generate all coordinate elements into the form when process is active.
+ foreach ($crop_properties as $property => $value) {
+ $crop_element = &$element['crop_wrapper'][$crop_type_id][$element_wrapper_name]['values'][$property];
+ $value_property = self::getCropFormPropertyValue($element, $crop_type_id, $edit, $value['value'], $property);
+ $crop_element = [
+ '#type' => 'hidden',
+ '#attributes' => [
+ 'class' => ["crop-$property"],
+ ],
+ '#crop_type' => $crop_type_id,
+ '#element_name' => $property,
+ '#default_value' => $value_property,
+ ];
+
+ if ($property == 'height' || $property == 'width') {
+ $crop_element['#element_validate'] = [
+ [
+ static::class,
+ 'validateHardLimit',
+ ],
+ ];
+ }
+ }
+ return $element;
+ }
+
+ /**
+ * Update crop elements of crop into the form.
+ *
+ * @param array $original_properties
+ * All properties calculate for apply to.
+ * @param bool $edit
+ * Context of this form.
+ *
+ * @return array<string,array>
+ * Populate all crop elements into the form.
+ */
+ public static function getCropFormProperties(array $original_properties, $edit) {
+ $crop_elements = self::setCoordinatesElement();
+ if (!empty($original_properties) && $edit) {
+ foreach ($crop_elements as $properties => $value) {
+ $crop_elements[$properties]['value'] = $original_properties[$properties];
+ }
+ }
+ return $crop_elements;
+ }
+
+ /**
+ * Get default value of property elements.
+ *
+ * @param array $element
+ * All form elements without crop properties.
+ * @param string $crop_type
+ * The id of the current crop.
+ * @param bool $edit
+ * Context of this form.
+ * @param int|null $value
+ * The values calculated by ImageCrop::getCropFormProperties().
+ * @param string $property
+ * Name of current property @see setCoordinatesElement().
+ *
+ * @return int|null
+ * Value of this element.
+ */
+ public static function getCropFormPropertyValue(array &$element, $crop_type, $edit, $value, $property) {
+ // Standard case.
+ if (!empty($edit) && isset($value)) {
+ return $value;
+ }
+ // Populate value when ajax populates values after process.
+ if (isset($element['#value']) && isset($element['crop_wrapper'])) {
+ $ajax_element = &$element['#value']['crop_wrapper']['container'][$crop_type]['values'];
+ return (isset($ajax_element[$property]) && !empty($ajax_element[$property])) ? $ajax_element[$property] : NULL;
+ }
+ return NULL;
+ }
+
+ /**
+ * Form element validation handler for crop widget elements.
+ *
+ * @param array $element
+ * All form elements without crop properties.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @see ImageCrop::getCropFormElement()
+ */
+ public static function validateHardLimit(array $element, FormStateInterface $form_state) {
+ /** @var \Drupal\crop\Entity\CropType $crop_type */
+ $crop_type = \Drupal::entityTypeManager()
+ ->getStorage('crop_type')
+ ->load($element['#crop_type']);
+ $parents = $element['#parents'];
+ array_pop($parents);
+ $crop_values = $form_state->getValue($parents);
+ $hard_limit = $crop_type->getHardLimit();
+ $action_button = $form_state->getTriggeringElement()['#value'];
+ // @todo We need to add this test in multilingual context because,
+ // the "#value" element are a simple string in translate form,
+ // and an TranslatableMarkup object in other cases.
+ $operation = ($action_button instanceof TranslatableMarkup) ? $action_button->getUntranslatedString() : $action_button;
+
+ if ((int) $crop_values['crop_applied'] == 0 || $operation == 'Remove') {
+ return;
+ }
+
+ $element_name = $element['#element_name'];
+ if ($hard_limit[$element_name] !== 0 && !empty($hard_limit[$element_name])) {
+ if ($hard_limit[$element_name] > (int) $crop_values[$element_name]) {
+ $form_state->setError($element, t('Crop @property is smaller then the allowed @hard_limitpx for @crop_name',
+ [
+ '@property' => $element_name,
+ '@hard_limit' => $hard_limit[$element_name],
+ '@crop_name' => $crop_type->label(),
+ ]
+ ));
+ }
+ }
+ }
+
+ /**
+ * Set All sizes properties of the crops.
+ *
+ * @return array<string,array>
+ * Set all possible crop zone properties.
+ */
+ public static function setCoordinatesElement() {
+ return [
+ 'x' => ['label' => t('X coordinate'), 'value' => NULL],
+ 'y' => ['label' => t('Y coordinate'), 'value' => NULL],
+ 'width' => ['label' => t('Width'), 'value' => NULL],
+ 'height' => ['label' => t('Height'), 'value' => NULL],
+ ];
+ }
+
+}