3 namespace Drupal\paragraphs\Plugin\Field\FieldWidget;
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Core\Entity\Entity\EntityFormDisplay;
8 use Drupal\Core\Entity\EntityInterface;
9 use Drupal\Core\Entity\RevisionableInterface;
10 use Drupal\Core\Field\FieldDefinitionInterface;
11 use Drupal\Core\Field\FieldFilteredMarkup;
12 use Drupal\Core\Field\FieldStorageDefinitionInterface;
13 use Drupal\Core\Field\WidgetBase;
14 use Drupal\Core\Form\FormStateInterface;
15 use Drupal\Core\Field\FieldItemListInterface;
16 use Drupal\Core\Render\Element;
17 use Drupal\node\Entity\Node;
18 use Drupal\paragraphs;
19 use Drupal\paragraphs\ParagraphInterface;
20 use Symfony\Component\Validator\ConstraintViolationInterface;
24 * Plugin implementation of the 'entity_reference paragraphs' widget.
26 * We hide add / remove buttons when translating to avoid accidental loss of
27 * data because these actions effect all languages.
30 * id = "entity_reference_paragraphs",
31 * label = @Translation("Paragraphs Classic"),
32 * description = @Translation("A paragraphs inline form widget."),
34 * "entity_reference_revisions"
38 class InlineParagraphsWidget extends WidgetBase {
41 * Indicates whether the current widget instance is in translation.
45 private $isTranslating;
48 * Id to name ajax buttons that includes field parents and field name.
52 protected $fieldIdPrefix;
55 * Wrapper id to identify the paragraphs.
59 protected $fieldWrapperId;
62 * Number of paragraphs item on form.
66 protected $realItemCount;
69 * Parents for the current paragraph.
73 protected $fieldParents;
76 * Accessible paragraphs types.
80 protected $accessOptions = NULL;
85 public static function defaultSettings() {
87 'title' => t('Paragraph'),
88 'title_plural' => t('Paragraphs'),
89 'edit_mode' => 'open',
90 'add_mode' => 'dropdown',
91 'form_display_mode' => 'default',
92 'default_paragraph_type' => '',
99 public function settingsForm(array $form, FormStateInterface $form_state) {
102 $elements['title'] = array(
103 '#type' => 'textfield',
104 '#title' => $this->t('Paragraph Title'),
105 '#description' => $this->t('Label to appear as title on the button as "Add new [title]", this label is translatable'),
106 '#default_value' => $this->getSetting('title'),
110 $elements['title_plural'] = array(
111 '#type' => 'textfield',
112 '#title' => $this->t('Plural Paragraph Title'),
113 '#description' => $this->t('Title in its plural form.'),
114 '#default_value' => $this->getSetting('title_plural'),
118 $elements['edit_mode'] = array(
120 '#title' => $this->t('Edit mode'),
121 '#description' => $this->t('The mode the paragraph is in by default. Preview will render the paragraph in the preview view mode.'),
123 'open' => $this->t('Open'),
124 'closed' => $this->t('Closed'),
125 'preview' => $this->t('Preview'),
127 '#default_value' => $this->getSetting('edit_mode'),
131 $elements['add_mode'] = array(
133 '#title' => $this->t('Add mode'),
134 '#description' => $this->t('The way to add new paragraphs.'),
136 'select' => $this->t('Select list'),
137 'button' => $this->t('Buttons'),
138 'dropdown' => $this->t('Dropdown button')
140 '#default_value' => $this->getSetting('add_mode'),
144 $elements['form_display_mode'] = array(
146 '#options' => \Drupal::service('entity_display.repository')->getFormModeOptions($this->getFieldSetting('target_type')),
147 '#description' => $this->t('The form display mode to use when rendering the paragraph form.'),
148 '#title' => $this->t('Form display mode'),
149 '#default_value' => $this->getSetting('form_display_mode'),
154 foreach ($this->getAllowedTypes() as $key => $bundle) {
155 $options[$key] = $bundle['label'];
158 $elements['default_paragraph_type'] = [
160 '#title' => $this->t('Default paragraph type'),
161 '#empty_value' => '_none',
162 '#default_value' => $this->getDefaultParagraphTypeMachineName(),
163 '#options' => $options,
164 '#description' => $this->t('When creating a new host entity, a paragraph of this type is added.'),
173 public function settingsSummary() {
175 $summary[] = $this->t('Title: @title', ['@title' => $this->getSetting('title')]);
176 $summary[] = $this->t('Plural title: @title_plural', [
177 '@title_plural' => $this->getSetting('title_plural')
180 switch($this->getSetting('edit_mode')) {
183 $edit_mode = $this->t('Open');
186 $edit_mode = $this->t('Closed');
189 $edit_mode = $this->t('Preview');
193 switch($this->getSetting('add_mode')) {
196 $add_mode = $this->t('Select list');
199 $add_mode = $this->t('Buttons');
202 $add_mode = $this->t('Dropdown button');
206 $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]);
207 $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]);
208 $summary[] = $this->t('Form display mode: @form_display_mode', [
209 '@form_display_mode' => $this->getSetting('form_display_mode')
211 if ($this->getDefaultParagraphTypeLabelName() !== NULL) {
212 $summary[] = $this->t('Default paragraph type: @default_paragraph_type', [
213 '@default_paragraph_type' => $this->getDefaultParagraphTypeLabelName()
223 * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation()
224 * Uses a similar approach to populate a new translation.
226 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
227 $field_name = $this->fieldDefinition->getName();
228 $parents = $element['#field_parents'];
231 $paragraphs_entity = NULL;
232 $host = $items->getEntity();
233 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
235 $entity_manager = \Drupal::entityTypeManager();
236 $target_type = $this->getFieldSetting('target_type');
238 $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit';
239 $default_edit_mode = $this->getSetting('edit_mode');
241 $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']);
243 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
244 $paragraphs_entity = $widget_state['paragraphs'][$delta]['entity'];
246 elseif (isset($items[$delta]->entity)) {
247 $paragraphs_entity = $items[$delta]->entity;
249 // We don't have a widget state yet, get from selector settings.
250 if (!isset($widget_state['paragraphs'][$delta]['mode'])) {
252 if ($default_edit_mode == 'open') {
255 elseif ($default_edit_mode == 'closed') {
256 $item_mode = 'closed';
258 elseif ($default_edit_mode == 'preview') {
259 $item_mode = 'preview';
263 elseif (isset($widget_state['selected_bundle'])) {
265 $entity_type = $entity_manager->getDefinition($target_type);
266 $bundle_key = $entity_type->getKey('bundle');
268 $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array(
269 $bundle_key => $widget_state['selected_bundle'],
275 if ($item_mode == 'collapsed') {
276 $item_mode = $default_edit_mode;
279 if ($item_mode == 'closed') {
280 // Validate closed paragraphs and expand if needed.
281 // @todo Consider recursion.
282 $violations = $paragraphs_entity->validate();
283 $violations->filterByFieldAccess();
284 if (count($violations) > 0) {
287 foreach ($violations as $violation) {
288 $messages[] = $violation->getMessage();
290 $info['validation_error'] = array(
291 '#type' => 'container',
292 '#markup' => $this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]),
293 '#attributes' => ['class' => ['messages', 'messages--warning']],
298 if ($paragraphs_entity) {
299 // Detect if we are translating.
300 $this->initIsTranslating($form_state, $host);
301 $langcode = $form_state->get('langcode');
303 if (!$this->isTranslating) {
304 // Set the langcode if we are not translating.
305 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
306 if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
307 // If a translation in the given language already exists, switch to
308 // that. If there is none yet, update the language.
309 if ($paragraphs_entity->hasTranslation($langcode)) {
310 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
313 $paragraphs_entity->set($langcode_key, $langcode);
318 // Add translation if missing for the target language.
319 if (!$paragraphs_entity->hasTranslation($langcode)) {
320 // Get the selected translation of the paragraph entity.
321 $entity_langcode = $paragraphs_entity->language()->getId();
322 $source = $form_state->get(['content_translation', 'source']);
323 $source_langcode = $source ? $source->getId() : $entity_langcode;
324 $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
325 // The paragraphs entity has no content translation source field if
326 // no paragraph entity field is translatable, even if the host is.
327 if ($paragraphs_entity->hasField('content_translation_source')) {
328 // Initialise the translation with source language values.
329 $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray());
330 $translation = $paragraphs_entity->getTranslation($langcode);
331 $manager = \Drupal::service('content_translation.manager');
332 $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId());
335 // If any paragraphs type is translatable do not switch.
336 if ($paragraphs_entity->hasField('content_translation_source')) {
337 // Switch the paragraph to the translation.
338 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
342 $element_parents = $parents;
343 $element_parents[] = $field_name;
344 $element_parents[] = $delta;
345 $element_parents[] = 'subform';
347 $id_prefix = implode('-', array_merge($parents, array($field_name, $delta)));
348 $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper');
351 '#type' => 'container',
352 '#element_validate' => array(array($this, 'elementValidate')),
354 '#type' => 'container',
355 '#parents' => $element_parents,
359 $element['#prefix'] = '<div id="' . $wrapper_id . '">';
360 $element['#suffix'] = '</div>';
362 $item_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
363 if (isset($item_bundles[$paragraphs_entity->bundle()])) {
364 $bundle_info = $item_bundles[$paragraphs_entity->bundle()];
366 $element['top'] = array(
367 '#type' => 'container',
369 '#attributes' => array(
371 'paragraph-type-top',
376 $element['top']['paragraph_type_title'] = array(
377 '#type' => 'container',
379 '#attributes' => array(
381 'paragraph-type-title',
386 $element['top']['paragraph_type_title']['info'] = array(
387 '#markup' => $bundle_info['label'],
393 // Hide the button when translating.
394 $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
395 if ($item_mode != 'remove') {
396 $links['remove_button'] = [
398 '#value' => $this->t('Remove'),
399 '#name' => strtr($id_prefix, '-', '_') . '_remove',
401 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
402 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
405 'callback' => [get_class($this), 'itemAjax'],
406 'wrapper' => $widget_state['ajax_wrapper_id'],
409 '#access' => $button_access,
410 '#prefix' => '<li class="remove">',
411 '#suffix' => '</li>',
412 '#paragraphs_mode' => 'remove',
417 if ($item_mode == 'edit') {
419 if (isset($items[$delta]->entity) && ($default_edit_mode == 'preview' || $default_edit_mode == 'closed')) {
420 $links['collapse_button'] = array(
422 '#value' => $this->t('Collapse'),
423 '#name' => strtr($id_prefix, '-', '_') . '_collapse',
425 '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')),
428 'callback' => array(get_class($this), 'itemAjax'),
429 'wrapper' => $widget_state['ajax_wrapper_id'],
432 '#access' => $paragraphs_entity->access('update'),
433 '#prefix' => '<li class="collapse">',
434 '#suffix' => '</li>',
435 '#paragraphs_mode' => 'collapsed',
436 '#paragraphs_show_warning' => TRUE,
440 // Hide the button when translating.
441 $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
443 $info['edit_button_info'] = array(
444 '#type' => 'container',
445 '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
446 '#attributes' => ['class' => ['messages', 'messages--warning']],
447 '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
450 $info['remove_button_info'] = array(
451 '#type' => 'container',
452 '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
453 '#attributes' => ['class' => ['messages', 'messages--warning']],
454 '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
457 $info['edit_remove_button_info'] = array(
458 '#type' => 'container',
459 '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
460 '#attributes' => ['class' => ['messages', 'messages--warning']],
461 '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
464 elseif ($item_mode == 'preview' || $item_mode == 'closed') {
465 $links['edit_button'] = array(
467 '#value' => $this->t('Edit'),
468 '#name' => strtr($id_prefix, '-', '_') . '_edit',
470 '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')),
471 '#limit_validation_errors' => array(array_merge($parents, array($field_name, 'add_more'))),
474 'callback' => array(get_class($this), 'itemAjax'),
475 'wrapper' => $widget_state['ajax_wrapper_id'],
478 '#access' => $paragraphs_entity->access('update'),
479 '#prefix' => '<li class="edit">',
480 '#suffix' => '</li>',
481 '#paragraphs_mode' => 'edit',
484 if ($show_must_be_saved_warning) {
485 $info['must_be_saved_info'] = array(
486 '#type' => 'container',
487 '#markup' => $this->t('You have unsaved changes on this @title item.', array('@title' => $this->getSetting('title'))),
488 '#attributes' => ['class' => ['messages', 'messages--warning']],
492 $info['preview_info'] = array(
493 '#type' => 'container',
494 '#markup' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
495 '#attributes' => ['class' => ['messages', 'messages--warning']],
496 '#access' => !$paragraphs_entity->access('view'),
499 $info['edit_button_info'] = array(
500 '#type' => 'container',
501 '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
502 '#attributes' => ['class' => ['messages', 'messages--warning']],
503 '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
506 $info['remove_button_info'] = array(
507 '#type' => 'container',
508 '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
509 '#attributes' => ['class' => ['messages', 'messages--warning']],
510 '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
513 $info['edit_remove_button_info'] = array(
514 '#type' => 'container',
515 '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
516 '#attributes' => ['class' => ['messages', 'messages--warning']],
517 '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
520 elseif ($item_mode == 'remove') {
522 $element['top']['paragraph_type_title']['info'] = [
523 '#markup' => $this->t('Deleted @title: %type', ['@title' => $this->getSetting('title'), '%type' => $bundle_info['label']]),
526 $links['confirm_remove_button'] = [
528 '#value' => $this->t('Confirm removal'),
529 '#name' => strtr($id_prefix, '-', '_') . '_confirm_remove',
531 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
532 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
535 'callback' => [get_class($this), 'itemAjax'],
536 'wrapper' => $widget_state['ajax_wrapper_id'],
539 '#prefix' => '<li class="confirm-remove">',
540 '#suffix' => '</li>',
541 '#paragraphs_mode' => 'removed',
544 $links['restore_button'] = [
546 '#value' => $this->t('Restore'),
547 '#name' => strtr($id_prefix, '-', '_') . '_restore',
549 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
550 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
553 'callback' => [get_class($this), 'itemAjax'],
554 'wrapper' => $widget_state['ajax_wrapper_id'],
557 '#prefix' => '<li class="restore">',
558 '#suffix' => '</li>',
559 '#paragraphs_mode' => 'edit',
565 foreach($links as $link_item) {
566 if (!isset($link_item['#access']) || $link_item['#access']) {
571 if ($show_links > 0) {
573 $element['top']['links'] = $links;
574 if ($show_links > 1) {
575 $element['top']['links']['#theme_wrappers'] = array('dropbutton_wrapper', 'paragraphs_dropbutton_wrapper');
576 $element['top']['links']['prefix'] = array(
577 '#markup' => '<ul class="dropbutton">',
580 $element['top']['links']['suffix'] = array(
581 '#markup' => '</li>',
586 $element['top']['links']['#theme_wrappers'] = array('paragraphs_dropbutton_wrapper');
587 foreach($links as $key => $link_item) {
588 unset($element['top']['links'][$key]['#prefix']);
589 unset($element['top']['links'][$key]['#suffix']);
592 $element['top']['links']['#weight'] = 2;
598 foreach($info as $info_item) {
599 if (!isset($info_item['#access']) || $info_item['#access']) {
606 $element['info'] = $info;
607 $element['info']['#weight'] = 998;
611 if (count($actions)) {
612 $show_actions = FALSE;
613 foreach($actions as $action_item) {
614 if (!isset($action_item['#access']) || $action_item['#access']) {
615 $show_actions = TRUE;
621 $element['actions'] = $actions;
622 $element['actions']['#type'] = 'actions';
623 $element['actions']['#weight'] = 999;
628 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
630 // @todo Remove as part of https://www.drupal.org/node/2640056
631 if (\Drupal::moduleHandler()->moduleExists('field_group')) {
633 'entity_type' => $paragraphs_entity->getEntityTypeId(),
634 'bundle' => $paragraphs_entity->bundle(),
635 'entity' => $paragraphs_entity,
637 'display_context' => 'form',
638 'mode' => $display->getMode(),
641 field_group_attach_groups($element['subform'], $context);
642 $element['subform']['#pre_render'][] = 'field_group_form_pre_render';
645 if ($item_mode == 'edit') {
646 $display->buildForm($paragraphs_entity, $element['subform'], $form_state);
647 foreach (Element::children($element['subform']) as $field) {
648 if ($paragraphs_entity->hasField($field)) {
649 $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable();
651 $element['subform'][$field]['widget']['#after_build'][] = [
653 'removeTranslatabilityClue'
659 elseif ($item_mode == 'preview') {
660 $element['subform'] = array();
661 $element['behavior_plugins'] = [];
662 $element['preview'] = entity_view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
663 $element['preview']['#access'] = $paragraphs_entity->access('view');
665 elseif ($item_mode == 'closed') {
666 $element['subform'] = array();
667 $element['behavior_plugins'] = [];
668 if ($paragraphs_entity) {
669 $summary = $this->addCollapsedSummary($paragraphs_entity);
670 $element['top']['paragraph_summary']['fields_info'] = [
671 '#markup' => $summary,
672 '#prefix' => '<div class="paragraphs-collapsed-description">',
673 '#suffix' => '</div>',
678 $element['subform'] = array();
681 $element['subform']['#attributes']['class'][] = 'paragraphs-subform';
682 $element['subform']['#access'] = $paragraphs_entity->access('update');
684 if ($item_mode == 'removed') {
685 $element['#access'] = FALSE;
688 $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity;
689 $widget_state['paragraphs'][$delta]['display'] = $display;
690 $widget_state['paragraphs'][$delta]['mode'] = $item_mode;
692 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
695 $element['#access'] = FALSE;
701 public function getAllowedTypes() {
703 $return_bundles = array();
705 $target_type = $this->getFieldSetting('target_type');
706 $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
708 if ($this->getSelectionHandlerSetting('target_bundles') !== NULL) {
709 $bundles = array_intersect_key($bundles, $this->getSelectionHandlerSetting('target_bundles'));
712 // Support for the paragraphs reference type.
713 $drag_drop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
714 if ($drag_drop_settings) {
715 $max_weight = count($bundles);
717 foreach ($drag_drop_settings as $bundle_info) {
718 if (isset($bundle_info['weight']) && $bundle_info['weight'] && $bundle_info['weight'] > $max_weight) {
719 $max_weight = $bundle_info['weight'];
723 // Default weight for new items.
724 $weight = $max_weight + 1;
725 foreach ($bundles as $machine_name => $bundle) {
726 $return_bundles[$machine_name] = array(
727 'label' => $bundle['label'],
728 'weight' => isset($drag_drop_settings[$machine_name]['weight']) ? $drag_drop_settings[$machine_name]['weight'] : $weight,
733 // Support for other reference types.
736 foreach ($bundles as $machine_name => $bundle) {
737 if (!count($this->getSelectionHandlerSetting('target_bundles'))
738 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles'))) {
740 $return_bundles[$machine_name] = array(
741 'label' => $bundle['label'],
750 uasort($return_bundles, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
752 return $return_bundles;
755 public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
756 $field_name = $this->fieldDefinition->getName();
757 $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
758 $this->fieldParents = $form['#parents'];
759 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
761 $max = $field_state['items_count'];
762 $entity_type_manager = \Drupal::entityTypeManager();
764 // Consider adding a default paragraph for new host entities.
765 if ($max == 0 && $items->getEntity()->isNew()) {
766 $default_type = $this->getDefaultParagraphTypeMachineName();
768 // Checking if default_type is not none and if is allowed.
770 // Place the default paragraph.
771 $target_type = $this->getFieldSetting('target_type');
772 $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create([
773 'type' => $default_type,
775 $field_state['selected_bundle'] = $default_type;
776 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
777 $field_state['paragraphs'][0] = [
778 'entity' => $paragraphs_entity,
779 'display' => $display,
781 'original_delta' => 1
784 $field_state['items_count'] = $max;
788 $this->realItemCount = $max;
789 $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
791 $title = $this->fieldDefinition->getLabel();
792 $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
795 $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name)));
796 $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper');
797 $elements['#prefix'] = '<div id="' . $this->fieldWrapperId . '">';
798 $elements['#suffix'] = '</div>';
800 $field_state['ajax_wrapper_id'] = $this->fieldWrapperId;
801 // Persist the widget state so formElement() can access it.
802 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
805 for ($delta = 0; $delta < $max; $delta++) {
807 // Add a new empty item if it doesn't exist yet at this delta.
808 if (!isset($items[$delta])) {
809 $items->appendItem();
812 // For multiple fields, title and description are handled by the wrapping
815 '#title' => $is_multiple ? '' : $title,
816 '#description' => $is_multiple ? '' : $description,
818 $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
821 // Input field for the delta (drag-n-drop reordering).
823 // We name the element '_weight' to avoid clashing with elements
824 // defined by widget.
825 $element['_weight'] = array(
827 '#title' => $this->t('Weight for row @number', array('@number' => $delta + 1)),
828 '#title_display' => 'invisible',
829 // Note: this 'delta' is the FAPI #type 'weight' element's property.
831 '#default_value' => $items[$delta]->_weight ?: $delta,
836 // Access for the top element is set to FALSE only when the paragraph
837 // was removed. A paragraphs that a user can not edit has access on
839 if (isset($element['#access']) && !$element['#access']) {
840 $this->realItemCount--;
843 $elements[$delta] = $element;
849 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
850 $field_state['real_item_count'] = $this->realItemCount;
851 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
853 if ($this->realItemCount > 0) {
855 '#theme' => 'field_multiple_value_form',
856 '#field_name' => $field_name,
857 '#cardinality' => $cardinality,
858 '#cardinality_multiple' => $is_multiple,
859 '#required' => $this->fieldDefinition->isRequired(),
861 '#description' => $description,
862 '#max_delta' => $max-1,
867 '#type' => 'container',
868 '#theme_wrappers' => ['container'],
869 '#field_name' => $field_name,
870 '#cardinality' => $cardinality,
871 '#cardinality_multiple' => TRUE,
872 '#max_delta' => $max-1,
874 '#type' => 'html_tag',
879 '#type' => 'container',
881 '#markup' => $this->t('No @title added yet.', ['@title' => $this->getSetting('title')]),
883 '#suffix' => '</em>',
889 $elements['description'] = [
890 '#type' => 'container',
891 'value' => ['#markup' => $description],
892 '#attributes' => ['class' => ['description']],
897 $host = $items->getEntity();
898 $this->initIsTranslating($form_state, $host);
900 if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) {
901 $elements['add_more'] = $this->buildAddActions();
904 $elements['#attached']['library'][] = 'paragraphs/drupal.paragraphs.admin';
912 public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
913 $parents = $form['#parents'];
915 // Identify the manage field settings default value form.
916 if (in_array('default_value_input', $parents, TRUE)) {
917 // Since the entity is not reusable neither cloneable, having a default
918 // value is not supported.
919 return ['#markup' => $this->t('No widget available for: %label.', ['%label' => $items->getFieldDefinition()->getLabel()])];
922 return parent::form($items, $form, $form_state, $get_delta);
926 * Add 'add more' button, if not working with a programmed form.
929 * The form element array.
931 protected function buildAddActions() {
932 if (count($this->getAccessibleOptions()) === 0) {
933 if (count($this->getAllowedTypes()) === 0) {
934 $add_more_elements['info'] = [
935 '#type' => 'container',
936 '#markup' => $this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]),
937 '#attributes' => ['class' => ['messages', 'messages--warning']],
941 $add_more_elements['info'] = [
942 '#type' => 'container',
943 '#markup' => $this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]),
944 '#attributes' => ['class' => ['messages', 'messages--warning']],
948 return $add_more_elements ;
951 if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') {
952 return $this->buildButtonsAddMode();
955 return $this->buildSelectAddMode();
959 * Returns the available paragraphs type.
962 * Available paragraphs types.
964 protected function getAccessibleOptions() {
965 if ($this->accessOptions !== NULL) {
966 return $this->accessOptions;
969 $entity_type_manager = \Drupal::entityTypeManager();
970 $target_type = $this->getFieldSetting('target_type');
971 $bundles = $this->getAllowedTypes();
972 $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
973 $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
975 foreach ($bundles as $machine_name => $bundle) {
976 if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
977 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
978 if ($access_control_handler->createAccess($machine_name)) {
979 $this->accessOptions[$machine_name] = $bundle['label'];
984 return $this->accessOptions;
988 * Builds dropdown button for adding new paragraph.
991 * The form element array.
993 protected function buildButtonsAddMode() {
994 // Hide the button when translating.
995 $add_more_elements = [
996 '#type' => 'container',
997 '#theme_wrappers' => ['paragraphs_dropbutton_wrapper'],
999 $field_name = $this->fieldDefinition->getName();
1000 $title = $this->fieldDefinition->getLabel();
1002 $drop_button = FALSE;
1003 if (count($this->getAccessibleOptions()) > 1 && $this->getSetting('add_mode') == 'dropdown') {
1004 $drop_button = TRUE;
1005 $add_more_elements['#theme_wrappers'] = ['dropbutton_wrapper'];
1006 $add_more_elements['prefix'] = [
1007 '#markup' => '<ul class="dropbutton">',
1010 $add_more_elements['suffix'] = [
1011 '#markup' => '</ul>',
1014 $add_more_elements['#suffix'] = $this->t(' to %type', ['%type' => $title]);
1017 foreach ($this->getAccessibleOptions() as $machine_name => $label) {
1018 $add_more_elements['add_more_button_' . $machine_name] = [
1019 '#type' => 'submit',
1020 '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_' . $machine_name . '_add_more',
1021 '#value' => $this->t('Add @type', ['@type' => $label]),
1022 '#attributes' => ['class' => ['field-add-more-submit']],
1023 '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1024 '#submit' => [[get_class($this), 'addMoreSubmit']],
1026 'callback' => [get_class($this), 'addMoreAjax'],
1027 'wrapper' => $this->fieldWrapperId,
1030 '#bundle_machine_name' => $machine_name,
1034 $add_more_elements['add_more_button_' . $machine_name]['#prefix'] = '<li>';
1035 $add_more_elements['add_more_button_' . $machine_name]['#suffix'] = '</li>';
1039 return $add_more_elements;
1043 * Builds list of actions based on paragraphs type.
1046 * The form element array.
1048 protected function buildSelectAddMode() {
1049 $field_name = $this->fieldDefinition->getName();
1050 $title = $this->fieldDefinition->getLabel();
1051 $add_more_elements['add_more_select'] = [
1052 '#type' => 'select',
1053 '#options' => $this->getAccessibleOptions(),
1054 '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]),
1055 '#label_display' => 'hidden',
1058 $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]);
1060 if ($this->realItemCount > 0) {
1061 $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]);
1064 $add_more_elements['add_more_button'] = [
1065 '#type' => 'submit',
1066 '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more',
1068 '#attributes' => ['class' => ['field-add-more-submit']],
1069 '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1070 '#submit' => [[get_class($this), 'addMoreSubmit']],
1072 'callback' => [get_class($this), 'addMoreAjax'],
1073 'wrapper' => $this->fieldWrapperId,
1078 $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]);
1079 return $add_more_elements;
1083 * Gets current language code from the form state or item.
1085 * Since the paragraph field is not set as translatable, the item language
1086 * code is set to the source language. The intended translation language
1087 * is only accessibly through the form state.
1089 * @param \Drupal\Core\Form\FormStateInterface $form_state
1090 * @param \Drupal\Core\Field\FieldItemListInterface $items
1093 protected function getCurrentLangcode(FormStateInterface $form_state, FieldItemListInterface $items) {
1094 return $form_state->get('langcode') ?: $items->getEntity()->language()->getId();
1100 public static function addMoreAjax(array $form, FormStateInterface $form_state) {
1101 $button = $form_state->getTriggeringElement();
1102 // Go one level up in the form, to the widgets container.
1103 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
1105 // Add a DIV around the delta receiving the Ajax effect.
1106 $delta = $element['#max_delta'];
1107 $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
1108 $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
1116 public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
1117 $button = $form_state->getTriggeringElement();
1119 // Go one level up in the form, to the widgets container.
1120 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
1121 $field_name = $element['#field_name'];
1122 $parents = $element['#field_parents'];
1124 // Increment the items count.
1125 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1127 if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
1128 $widget_state['items_count']++;
1131 if (isset($button['#bundle_machine_name'])) {
1132 $widget_state['selected_bundle'] = $button['#bundle_machine_name'];
1135 $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value'];
1138 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1140 $form_state->setRebuild();
1143 public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
1144 $button = $form_state->getTriggeringElement();
1146 // Go one level up in the form, to the widgets container.
1147 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
1149 $delta = array_slice($button['#array_parents'], -4, -3);
1152 $field_name = $element['#field_name'];
1153 $parents = $element['#field_parents'];
1155 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1157 $widget_state['paragraphs'][$delta]['mode'] = $button['#paragraphs_mode'];
1159 if (!empty($button['#paragraphs_show_warning'])) {
1160 $widget_state['paragraphs'][$delta]['show_warning'] = $button['#paragraphs_show_warning'];
1163 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1165 $form_state->setRebuild();
1168 public static function itemAjax(array $form, FormStateInterface $form_state) {
1169 $button = $form_state->getTriggeringElement();
1170 // Go one level up in the form, to the widgets container.
1171 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
1173 $element['#prefix'] = '<div class="ajax-new-content">' . (isset($element['#prefix']) ? $element['#prefix'] : '');
1174 $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>';
1182 public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
1187 * Returns the value of a setting for the entity reference selection handler.
1189 * @param string $setting_name
1193 * The setting value.
1195 protected function getSelectionHandlerSetting($setting_name) {
1196 $settings = $this->getFieldSetting('handler_settings');
1197 return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
1201 * Checks whether a content entity is referenced.
1205 protected function isContentReferenced() {
1206 $target_type = $this->getFieldSetting('target_type');
1207 $target_type_info = \Drupal::entityTypeManager()->getDefinition($target_type);
1208 return $target_type_info->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface');
1214 public function elementValidate($element, FormStateInterface $form_state, $form) {
1215 $field_name = $this->fieldDefinition->getName();
1216 $widget_state = static::getWidgetState($element['#field_parents'], $field_name, $form_state);
1217 $delta = $element['#delta'];
1219 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
1220 $entity = $widget_state['paragraphs'][$delta]['entity'];
1222 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1223 $display = $widget_state['paragraphs'][$delta]['display'];
1225 if ($widget_state['paragraphs'][$delta]['mode'] == 'edit') {
1226 // Extract the form values on submit for getting the current paragraph.
1227 $display->extractFormValues($entity, $element['subform'], $form_state);
1228 $display->validateFormValues($entity, $element['subform'], $form_state);
1232 static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state);
1238 public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
1239 $entity = $form_state->getFormObject()->getEntity();
1240 $field_name = $this->fieldDefinition->getName();
1241 $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1242 $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']);
1244 $new_revision = FALSE;
1245 if ($entity instanceof RevisionableInterface) {
1246 if ($entity->isNewRevision()) {
1247 $new_revision = TRUE;
1249 // Most of the time we don't know yet if the host entity is going to be
1250 // saved as a new revision using RevisionableInterface::isNewRevision().
1251 // Most entity types (at least nodes) however use a boolean property named
1252 // "revision" to indicate whether a new revision should be saved. Use that
1254 elseif ($entity->getEntityType()->hasKey('revision') && $form_state->getValue('revision')) {
1255 $new_revision = TRUE;
1259 foreach ($values as $delta => &$item) {
1260 if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
1261 && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
1262 $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity'];
1264 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1265 $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
1266 if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') {
1267 $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state);
1269 $paragraphs_entity->setNewRevision($new_revision);
1270 // A content entity form saves without any rebuild. It needs to set the
1271 // language to update it in case of language change.
1272 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
1273 if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) {
1274 // If a translation in the given language already exists, switch to
1275 // that. If there is none yet, update the language.
1276 if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) {
1277 $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode'));
1280 $paragraphs_entity->set($langcode_key, $form_state->get('langcode'));
1284 $paragraphs_entity->setNeedsSave(TRUE);
1285 $item['entity'] = $paragraphs_entity;
1286 $item['target_id'] = $paragraphs_entity->id();
1287 $item['target_revision_id'] = $paragraphs_entity->getRevisionId();
1289 // If our mode is remove don't save or reference this entity.
1290 // @todo: Maybe we should actually delete it here?
1291 elseif($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove' || $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'removed') {
1292 $item['target_id'] = NULL;
1293 $item['target_revision_id'] = NULL;
1302 public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
1303 // Filter possible empty items.
1304 $items->filterEmptyItems();
1305 return parent::extractFormValues($items, $form, $form_state);
1309 * Initializes the translation form state.
1311 * @param \Drupal\Core\Form\FormStateInterface $form_state
1312 * @param \Drupal\Core\Entity\EntityInterface $host
1314 protected function initIsTranslating(FormStateInterface $form_state, EntityInterface $host) {
1315 if ($this->isTranslating != NULL) {
1318 $this->isTranslating = FALSE;
1319 if (!$host->isTranslatable()) {
1322 if (!$host->getEntityType()->hasKey('default_langcode')) {
1325 $default_langcode_key = $host->getEntityType()->getKey('default_langcode');
1326 if (!$host->hasField($default_langcode_key)) {
1330 if (!empty($form_state->get('content_translation'))) {
1331 // Adding a language through the ContentTranslationController.
1332 $this->isTranslating = TRUE;
1334 if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) {
1335 // Editing a translation.
1336 $this->isTranslating = TRUE;
1341 * After-build callback for removing the translatability clue from the widget.
1343 * If the fields on the paragraph type are translatable,
1344 * ContentTranslationHandler::addTranslatabilityClue()adds an
1345 * "(all languages)" suffix to the widget title. That suffix is incorrect and
1346 * is being removed by this method using a #after_build on the field widget.
1348 * @param array $element
1349 * @param \Drupal\Core\Form\FormStateInterface $form_state
1353 public static function removeTranslatabilityClue(array $element, FormStateInterface $form_state) {
1354 // Widgets could have multiple elements with their own titles, so remove the
1355 // suffix if it exists, do not recurse lower than this to avoid going into
1356 // nested paragraphs or similar nested field types.
1357 $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>';
1358 if (isset($element['#title']) && strpos($element['#title'], $suffix)) {
1359 $element['#title'] = str_replace($suffix, '', $element['#title']);
1361 // Loop over all widget deltas.
1362 foreach (Element::children($element) as $delta) {
1363 if (isset($element[$delta]['#title']) && strpos($element[$delta]['#title'], $suffix)) {
1364 $element[$delta]['#title'] = str_replace($suffix, '', $element[$delta]['#title']);
1366 // Loop over all form elements within the current delta.
1367 foreach (Element::children($element[$delta]) as $field) {
1368 if (isset($element[$delta][$field]['#title']) && strpos($element[$delta][$field]['#title'], $suffix)) {
1369 $element[$delta][$field]['#title'] = str_replace($suffix, '', $element[$delta][$field]['#title']);
1377 * Returns the default paragraph type.
1379 * @return string $default_paragraph_type
1380 * Label name for default paragraph type.
1382 protected function getDefaultParagraphTypeLabelName(){
1383 if ($this->getDefaultParagraphTypeMachineName() !== NULL) {
1384 $allowed_types = $this->getAllowedTypes();
1385 return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label'];
1392 * Returns the machine name for default paragraph type.
1395 * Machine name for default paragraph type.
1397 protected function getDefaultParagraphTypeMachineName() {
1398 $default_type = $this->getSetting('default_paragraph_type');
1399 $allowed_types = $this->getAllowedTypes();
1400 if ($default_type && isset($allowed_types[$default_type])) {
1401 return $default_type;
1403 // Check if the user explicitly selected not to have any default Paragraph
1404 // type. Othewise, if there is only one type available, that one is the
1406 if ($default_type === '_none') {
1409 if (count($allowed_types) === 1) {
1410 return key($allowed_types);
1417 * @param \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity
1418 * Entity where to extract the values.
1420 * @return string $collapsed_summary_text
1421 * The text without tags to return.
1423 public function addCollapsedSummary(paragraphs\Entity\Paragraph $paragraphs_entity) {
1424 $text_types = ['text_with_summary', 'text', 'text_long', 'list_string'];
1426 foreach ($paragraphs_entity->getFieldDefinitions() as $key => $value) {
1427 if ($value->getType() == 'image') {
1428 if ($paragraphs_entity->get($key)->entity) {
1429 foreach ($paragraphs_entity->get($key) as $image_key => $image_value) {
1430 if ($image_value->title != '') {
1431 $text = $image_value->title;
1433 elseif ($image_value->alt != '') {
1434 $text = $image_value->alt;
1436 elseif ($text = $image_value->entity->filename->value) {
1437 $text = $image_value->entity->filename->value;
1439 if (strlen($text) > 50) {
1440 $text = strip_tags(substr($text, 0, 150));
1446 if (in_array($value->getType(), $text_types)) {
1447 $text = $paragraphs_entity->get($key)->value;
1448 if (strlen($text) > 50) {
1449 $text = strip_tags(substr($text, 0, 150));
1453 if ($field_type = $value->getType() == 'entity_reference_revisions') {
1454 if ($paragraphs_entity->get($key) && $paragraphs_entity->get($key)->entity) {
1455 $summary[] = $this->addCollapsedSummary($paragraphs_entity->get($key)->entity);
1459 $collapsed_summary_text = implode(', ', $summary);
1460 return strip_tags($collapsed_summary_text);
1466 public static function isApplicable(FieldDefinitionInterface $field_definition) {
1467 $target_type = $field_definition->getSetting('target_type');
1468 $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type);
1469 if ($paragraph_type) {
1470 return $paragraph_type->isSubclassOf(ParagraphInterface::class);