You are here

class ImageCrop in Image Widget Crop 8

Same name and namespace in other branches
  1. 8.2 src/Element/ImageCrop.php \Drupal\image_widget_crop\Element\ImageCrop

Provides a form element for crop.

Plugin annotation

@FormElement("image_crop");

Hierarchy

Expanded class hierarchy of ImageCrop

2 #type uses of ImageCrop
ImageCropWidget::process in src/Plugin/Field/FieldWidget/ImageCropWidget.php
Form API callback: Processes a crop_image field element.
image_widget_crop_form_file_form_alter in ./image_widget_crop.module
Implements hook_form_alter().

File

src/Element/ImageCrop.php, line 20

Namespace

Drupal\image_widget_crop\Element
View source
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,
      ],
    ];
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormElement::processAutocomplete public static function Adds autocomplete functionality to elements.
FormElement::processPattern public static function #process callback for #pattern form element property.
FormElement::validatePattern public static function #element_validate callback for #pattern form element property.
ImageCrop::countFileUsages public static function Counts how many times a file has been used.
ImageCrop::getCropFormElement public static function Inject crop elements into the form.
ImageCrop::getCropFormProperties public static function Update crop elements of crop into the form.
ImageCrop::getCropFormPropertyValue public static function Get default value of property elements.
ImageCrop::getInfo public function Returns the element properties for this element. Overrides ElementInterface::getInfo
ImageCrop::processCrop public static function Render API callback: Expands the image_crop element type.
ImageCrop::setCoordinatesElement public static function Set All sizes properties of the crops.
ImageCrop::validateHardLimit public static function Form element validation handler for crop widget elements.
ImageCrop::valueCallback public static function Determines how user input is mapped to an element's #value property. Overrides FormElement::valueCallback
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginBase::__construct public function Constructs a \Drupal\Component\Plugin\PluginBase object. 92
RenderElement::preRenderAjaxForm public static function Adds Ajax information about an element to communicate with JavaScript.
RenderElement::preRenderGroup public static function Adds members of this group as actual elements for rendering.
RenderElement::processAjaxForm public static function Form element processing handler for the #ajax form property. 1
RenderElement::processGroup public static function Arranges elements into groups.
RenderElement::setAttributes public static function Sets a form element's class attribute. Overrides ElementInterface::setAttributes
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.