3 namespace Drupal\image_widget_crop\Element;
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;
16 * Provides a form element for crop.
18 * @FormElement("image_crop")
20 class ImageCrop extends FormElement {
25 public function getInfo() {
28 [static::class, 'processCrop'],
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,
37 'library' => 'image_widget_crop/cropper.integration',
46 public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
55 * Render API callback: Expands the image_crop element type.
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'];
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'],
73 'class' => ['messages messages--error'],
76 // Stop image_crop process and display error message.
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);
88 $element['crop_wrapper'] = [
90 '#title' => t('Crop image'),
91 '#attributes' => ['class' => ['image-data__crop-wrapper']],
92 '#open' => $element['#show_crop_area'],
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'),
104 'class' => ['messages messages--warning'],
111 // Ensure that the ID of an element is unique.
112 $list_id = \Drupal::service('uuid')->generate();
114 $element['crop_wrapper'][$list_id] = [
115 '#type' => 'vertical_tabs',
116 '#theme_wrappers' => ['vertical_tabs'],
117 '#parents' => [$list_id],
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';
128 $element['#attached']['drupalSettings']['image_widget_crop'][$crop_type->id()] = [
129 'soft_limit' => $crop_type->getSoftLimit(),
130 'hard_limit' => $crop_type->getHardLimit(),
133 $element['crop_wrapper'][$crop_type->id()] = [
134 '#type' => 'details',
135 '#title' => $crop_type->label(),
136 '#group' => $list_id,
139 // Generation of html List with image & crop information.
140 $element['crop_wrapper'][$crop_type->id()]['crop_container'] = [
141 '#type' => 'container',
143 'class' => ['crop-preview-wrapper', $list_id],
144 'id' => [$crop_type->id()],
145 'data-ratio' => [$ratio],
150 $element['crop_wrapper'][$crop_type->id()]['crop_container']['image'] = [
151 '#theme' => 'image_style',
152 '#style_name' => $element['#crop_preview_image_style'],
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],
160 '#uri' => $file->getFileUri(),
164 $element['crop_wrapper'][$crop_type->id()]['crop_container']['reset'] = [
166 '#value' => t('Reset crop'),
167 '#attributes' => ['class' => ['crop-preview-wrapper__crop-reset']],
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']],
178 // Element to track whether cropping is applied or not.
179 $element['crop_wrapper'][$crop_type->id()]['crop_container']['values']['crop_applied'] = [
181 '#attributes' => ['class' => ["crop-applied"]],
182 '#default_value' => 0,
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;
195 $properties = $form_state_properties;
198 /** @var \Drupal\crop\Entity\Crop $crop */
199 $crop = Crop::findCrop($file->getFileUri(), $crop_type->id());
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);
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;
217 self::getCropFormElement($element, 'crop_container', $properties, $edit, $crop_type->id());
219 // Stock Original File Values.
220 $element['file-uri'] = [
222 '#value' => $file->getFileUri(),
225 $element['file-id'] = [
227 '#value' => $file->id(),
235 * Counts how many times a file has been used.
237 * @param \Drupal\file\FileInterface $file
238 * The file entity to check usages.
241 * Returns how many times the file has been used.
243 public static function countFileUsages(FileInterface $file) {
245 $file_usage = \Drupal::service('file.usage')->listUsage($file);
246 foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($file_usage)) as $usage) {
247 $counter += (int) $usage;
253 * Inject crop elements into the form.
255 * @param array $element
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.
262 * Context of this form.
263 * @param string $crop_type_id
264 * The id of the current crop.
267 * Populate all crop elements into the form.
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);
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);
279 'class' => ["crop-$property"],
281 '#crop_type' => $crop_type_id,
282 '#element_name' => $property,
283 '#default_value' => $value_property,
286 if ($property == 'height' || $property == 'width') {
287 $crop_element['#element_validate'] = [
299 * Update crop elements of crop into the form.
301 * @param array $original_properties
302 * All properties calculate for apply to.
304 * Context of this form.
306 * @return array<string,array>
307 * Populate all crop elements into the form.
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];
316 return $crop_elements;
320 * Get default value of property elements.
322 * @param array $element
323 * All form elements without crop properties.
324 * @param string $crop_type
325 * The id of the current crop.
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().
334 * Value of this element.
336 public static function getCropFormPropertyValue(array &$element, $crop_type, $edit, $value, $property) {
338 if (!empty($edit) && isset($value)) {
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;
350 * Form element validation handler for crop widget elements.
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.
357 * @see ImageCrop::getCropFormElement()
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'];
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;
374 if ((int) $crop_values['crop_applied'] == 0 || $operation == 'Remove') {
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',
383 '@property' => $element_name,
384 '@hard_limit' => $hard_limit[$element_name],
385 '@crop_name' => $crop_type->label(),
393 * Set All sizes properties of the crops.
395 * @return array<string,array>
396 * Set all possible crop zone properties.
398 public static function setCoordinatesElement() {
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],