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\Form\SubformState;
17 use Drupal\Core\Render\Element;
18 use Drupal\paragraphs;
19 use Drupal\paragraphs\ParagraphInterface;
22 * Plugin implementation of the 'entity_reference_revisions paragraphs' widget.
26 * label = @Translation("Paragraphs EXPERIMENTAL"),
27 * description = @Translation("An experimental paragraphs inline form widget."),
29 * "entity_reference_revisions"
33 class ParagraphsWidget extends WidgetBase {
36 * Indicates whether the current widget instance is in translation.
40 protected $isTranslating;
43 * Id to name ajax buttons that includes field parents and field name.
47 protected $fieldIdPrefix;
50 * Wrapper id to identify the paragraphs.
54 protected $fieldWrapperId;
57 * Number of paragraphs item on form.
61 protected $realItemCount;
64 * Parents for the current paragraph.
68 protected $fieldParents;
71 * Accessible paragraphs types.
75 protected $accessOptions = NULL;
80 public static function defaultSettings() {
82 'title' => t('Paragraph'),
83 'title_plural' => t('Paragraphs'),
84 'edit_mode' => 'open',
85 'add_mode' => 'dropdown',
86 'form_display_mode' => 'default',
87 'default_paragraph_type' => '',
94 public function settingsForm(array $form, FormStateInterface $form_state) {
97 $elements['title'] = array(
98 '#type' => 'textfield',
99 '#title' => $this->t('Paragraph Title'),
100 '#description' => $this->t('Label to appear as title on the button as "Add new [title]", this label is translatable'),
101 '#default_value' => $this->getSetting('title'),
105 $elements['title_plural'] = array(
106 '#type' => 'textfield',
107 '#title' => $this->t('Plural Paragraph Title'),
108 '#description' => $this->t('Title in its plural form.'),
109 '#default_value' => $this->getSetting('title_plural'),
113 $elements['edit_mode'] = array(
115 '#title' => $this->t('Edit mode'),
116 '#description' => $this->t('The mode the paragraph is in by default. Preview will render the paragraph in the preview view mode.'),
118 'open' => $this->t('Open'),
119 'closed' => $this->t('Closed'),
120 'preview' => $this->t('Preview'),
122 '#default_value' => $this->getSetting('edit_mode'),
126 $elements['add_mode'] = array(
128 '#title' => $this->t('Add mode'),
129 '#description' => $this->t('The way to add new paragraphs.'),
131 'select' => $this->t('Select list'),
132 'button' => $this->t('Buttons'),
133 'dropdown' => $this->t('Dropdown button')
135 '#default_value' => $this->getSetting('add_mode'),
139 $elements['form_display_mode'] = array(
141 '#options' => \Drupal::service('entity_display.repository')->getFormModeOptions($this->getFieldSetting('target_type')),
142 '#description' => $this->t('The form display mode to use when rendering the paragraph form.'),
143 '#title' => $this->t('Form display mode'),
144 '#default_value' => $this->getSetting('form_display_mode'),
149 foreach ($this->getAllowedTypes() as $key => $bundle) {
150 $options[$key] = $bundle['label'];
153 $elements['default_paragraph_type'] = [
155 '#title' => $this->t('Default paragraph type'),
156 '#empty_value' => '_none',
157 '#default_value' => $this->getDefaultParagraphTypeMachineName(),
158 '#options' => $options,
159 '#description' => $this->t('When creating a new host entity, a paragraph of this type is added.'),
168 public function settingsSummary() {
170 $summary[] = $this->t('Title: @title', ['@title' => $this->getSetting('title')]);
171 $summary[] = $this->t('Plural title: @title_plural', [
172 '@title_plural' => $this->getSetting('title_plural')
175 switch($this->getSetting('edit_mode')) {
178 $edit_mode = $this->t('Open');
181 $edit_mode = $this->t('Closed');
184 $edit_mode = $this->t('Preview');
188 switch($this->getSetting('add_mode')) {
191 $add_mode = $this->t('Select list');
194 $add_mode = $this->t('Buttons');
197 $add_mode = $this->t('Dropdown button');
201 $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]);
202 $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]);
203 $summary[] = $this->t('Form display mode: @form_display_mode', [
204 '@form_display_mode' => $this->getSetting('form_display_mode')
206 if ($this->getDefaultParagraphTypeLabelName() !== NULL) {
207 $summary[] = $this->t('Default paragraph type: @default_paragraph_type', [
208 '@default_paragraph_type' => $this->getDefaultParagraphTypeLabelName()
218 * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation()
219 * Uses a similar approach to populate a new translation.
221 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
222 $field_name = $this->fieldDefinition->getName();
223 $parents = $element['#field_parents'];
226 $paragraphs_entity = NULL;
227 $host = $items->getEntity();
228 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
230 $entity_manager = \Drupal::entityTypeManager();
231 $target_type = $this->getFieldSetting('target_type');
233 $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit';
234 $default_edit_mode = $this->getSetting('edit_mode');
236 $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']);
238 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
239 $paragraphs_entity = $widget_state['paragraphs'][$delta]['entity'];
241 elseif (isset($items[$delta]->entity)) {
242 $paragraphs_entity = $items[$delta]->entity;
244 // We don't have a widget state yet, get from selector settings.
245 if (!isset($widget_state['paragraphs'][$delta]['mode'])) {
247 if ($default_edit_mode == 'open') {
250 elseif ($default_edit_mode == 'closed') {
251 $item_mode = 'closed';
253 elseif ($default_edit_mode == 'preview') {
254 $item_mode = 'preview';
258 elseif (isset($widget_state['selected_bundle'])) {
260 $entity_type = $entity_manager->getDefinition($target_type);
261 $bundle_key = $entity_type->getKey('bundle');
263 $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array(
264 $bundle_key => $widget_state['selected_bundle'],
270 if ($item_mode == 'closed') {
271 // Validate closed paragraphs and expand if needed.
272 // @todo Consider recursion.
273 $violations = $paragraphs_entity->validate();
274 $violations->filterByFieldAccess();
275 if (count($violations) > 0) {
278 foreach ($violations as $violation) {
279 $messages[] = $violation->getMessage();
281 $info['validation_error'] = array(
282 '#type' => 'container',
283 '#markup' => $this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]),
284 '#attributes' => ['class' => ['messages', 'messages--warning']],
289 if ($paragraphs_entity) {
290 // Detect if we are translating.
291 $this->initIsTranslating($form_state, $host);
292 $langcode = $form_state->get('langcode');
294 if (!$this->isTranslating) {
295 // Set the langcode if we are not translating.
296 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
297 if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
298 // If a translation in the given language already exists, switch to
299 // that. If there is none yet, update the language.
300 if ($paragraphs_entity->hasTranslation($langcode)) {
301 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
304 $paragraphs_entity->set($langcode_key, $langcode);
309 // Add translation if missing for the target language.
310 if (!$paragraphs_entity->hasTranslation($langcode)) {
311 // Get the selected translation of the paragraph entity.
312 $entity_langcode = $paragraphs_entity->language()->getId();
313 $source = $form_state->get(['content_translation', 'source']);
314 $source_langcode = $source ? $source->getId() : $entity_langcode;
315 $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
316 // The paragraphs entity has no content translation source field if
317 // no paragraph entity field is translatable, even if the host is.
318 if ($paragraphs_entity->hasField('content_translation_source')) {
319 // Initialise the translation with source language values.
320 $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray());
321 $translation = $paragraphs_entity->getTranslation($langcode);
322 $manager = \Drupal::service('content_translation.manager');
323 $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId());
326 // If any paragraphs type is translatable do not switch.
327 if ($paragraphs_entity->hasField('content_translation_source')) {
328 // Switch the paragraph to the translation.
329 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
333 $element_parents = $parents;
334 $element_parents[] = $field_name;
335 $element_parents[] = $delta;
336 $element_parents[] = 'subform';
338 $id_prefix = implode('-', array_merge($parents, array($field_name, $delta)));
339 $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper');
342 '#type' => 'container',
343 '#element_validate' => array(array($this, 'elementValidate')),
345 '#type' => 'container',
346 '#parents' => $element_parents,
350 $element['#prefix'] = '<div id="' . $wrapper_id . '">';
351 $element['#suffix'] = '</div>';
353 $item_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
354 if (isset($item_bundles[$paragraphs_entity->bundle()])) {
355 $bundle_info = $item_bundles[$paragraphs_entity->bundle()];
357 $element['top'] = array(
358 '#type' => 'container',
360 '#attributes' => array(
362 'paragraph-type-top',
367 $element['top']['paragraph_type_title'] = array(
368 '#type' => 'container',
370 '#attributes' => array(
372 'paragraph-type-title',
377 $element['top']['paragraph_type_title']['info'] = array(
378 '#markup' => $bundle_info['label'],
384 $links['duplicate_button'] = [
386 '#value' => $this->t('Duplicate'),
387 '#name' => strtr($id_prefix, '-', '_') . '_duplicate',
389 '#submit' => [[get_class($this), 'duplicateSubmit']],
390 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
393 'callback' => [get_class($this), 'itemAjax'],
394 'wrapper' => $widget_state['ajax_wrapper_id'],
397 '#access' => $paragraphs_entity->access('update'),
398 '#prefix' => '<li class="duplicate">',
399 '#suffix' => '</li>',
402 // Hide the button when translating.
403 $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
404 if($item_mode != 'remove') {
405 $links['remove_button'] = [
407 '#value' => $this->t('Remove'),
408 '#name' => strtr($id_prefix, '-', '_') . '_remove',
410 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
411 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
414 'callback' => array(get_class($this), 'itemAjax'),
415 'wrapper' => $widget_state['ajax_wrapper_id'],
418 '#access' => $button_access,
419 '#prefix' => '<li class="remove">',
420 '#suffix' => '</li>',
421 '#paragraphs_mode' => 'remove',
425 if ($item_mode == 'edit') {
427 if (isset($paragraphs_entity)) {
428 $links['collapse_button'] = array(
430 '#value' => $this->t('Collapse'),
431 '#name' => strtr($id_prefix, '-', '_') . '_collapse',
433 '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')),
436 'callback' => array(get_class($this), 'itemAjax'),
437 'wrapper' => $widget_state['ajax_wrapper_id'],
440 '#access' => $paragraphs_entity->access('update'),
441 '#prefix' => '<li class="collapse">',
442 '#suffix' => '</li>',
443 '#paragraphs_mode' => 'closed',
444 '#paragraphs_show_warning' => TRUE,
448 $info['edit_button_info'] = array(
449 '#type' => 'container',
450 '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
451 '#attributes' => ['class' => ['messages', 'messages--warning']],
452 '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
455 $info['remove_button_info'] = array(
456 '#type' => 'container',
457 '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
458 '#attributes' => ['class' => ['messages', 'messages--warning']],
459 '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
462 $info['edit_remove_button_info'] = array(
463 '#type' => 'container',
464 '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
465 '#attributes' => ['class' => ['messages', 'messages--warning']],
466 '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
470 $element['top']['paragraphs_edit_button_container'] = [
471 '#type' => 'container',
475 'paragraphs-edit-button-container',
478 'paragraphs_edit_button' => [
480 '#value' => $this->t('Edit'),
481 '#name' => strtr($id_prefix, '-', '_') . '_edit',
485 'paragraphs-edit-button',
488 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
489 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
492 'callback' => [get_class($this), 'itemAjax'],
493 'wrapper' => $widget_state['ajax_wrapper_id'],
496 '#access' => $paragraphs_entity->access('update'),
497 '#paragraphs_mode' => 'edit',
501 if ($show_must_be_saved_warning) {
502 $info['must_be_saved_info'] = array(
503 '#type' => 'container',
504 '#markup' => $this->t('You have unsaved changes on this @title item.', array('@title' => $this->getSetting('title'))),
505 '#attributes' => ['class' => ['messages', 'messages--warning']],
509 $info['preview_info'] = array(
510 '#type' => 'container',
511 '#markup' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
512 '#attributes' => ['class' => ['messages', 'messages--warning']],
513 '#access' => !$paragraphs_entity->access('view'),
516 $info['edit_button_info'] = array(
517 '#type' => 'container',
518 '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
519 '#attributes' => ['class' => ['messages', 'messages--warning']],
520 '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
523 $info['remove_button_info'] = array(
524 '#type' => 'container',
525 '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
526 '#attributes' => ['class' => ['messages', 'messages--warning']],
527 '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
530 $info['edit_remove_button_info'] = array(
531 '#type' => 'container',
532 '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
533 '#attributes' => ['class' => ['messages', 'messages--warning']],
534 '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
540 foreach($links as $link_item) {
541 if (!isset($link_item['#access']) || $link_item['#access']) {
546 if ($show_links > 0) {
548 $element['top']['links'] = $links;
549 if ($show_links > 1) {
550 $element['top']['links']['#theme_wrappers'] = array('dropbutton_wrapper', 'paragraphs_dropbutton_wrapper');
551 $element['top']['links']['prefix'] = array(
552 '#markup' => '<ul class="dropbutton">',
555 $element['top']['links']['suffix'] = array(
556 '#markup' => '</li>',
561 $element['top']['links']['#theme_wrappers'] = array('paragraphs_dropbutton_wrapper');
562 foreach($links as $key => $link_item) {
563 unset($element['top']['links'][$key]['#prefix']);
564 unset($element['top']['links'][$key]['#suffix']);
567 $element['top']['links']['#weight'] = 1;
573 foreach($info as $info_item) {
574 if (!isset($info_item['#access']) || $info_item['#access']) {
581 $element['info'] = $info;
582 $element['info']['#weight'] = 998;
586 if (count($actions)) {
587 $show_actions = FALSE;
588 foreach($actions as $action_item) {
589 if (!isset($action_item['#access']) || $action_item['#access']) {
590 $show_actions = TRUE;
596 $element['actions'] = $actions;
597 $element['actions']['#type'] = 'actions';
598 $element['actions']['#weight'] = 999;
603 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
605 // @todo Remove as part of https://www.drupal.org/node/2640056
606 if (\Drupal::moduleHandler()->moduleExists('field_group')) {
608 'entity_type' => $paragraphs_entity->getEntityTypeId(),
609 'bundle' => $paragraphs_entity->bundle(),
610 'entity' => $paragraphs_entity,
612 'display_context' => 'form',
613 'mode' => $display->getMode(),
616 field_group_attach_groups($element['subform'], $context);
617 $element['subform']['#pre_render'][] = 'field_group_form_pre_render';
620 if ($item_mode == 'edit') {
621 $display->buildForm($paragraphs_entity, $element['subform'], $form_state);
622 foreach (Element::children($element['subform']) as $field) {
623 if ($paragraphs_entity->hasField($field)) {
624 $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable();
626 $element['subform'][$field]['widget']['#after_build'][] = [
628 'removeTranslatabilityClue'
634 // Build the behavior plugins fields.
635 $paragraphs_type = $paragraphs_entity->getParagraphType();
636 if ($paragraphs_type) {
637 foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
638 $element['behavior_plugins'][$plugin_id] = [];
639 $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form, $form_state);
640 if ($plugin_form = $plugin->buildBehaviorForm($paragraphs_entity, $element['behavior_plugins'][$plugin_id], $subform_state)) {
641 $element['behavior_plugins'][$plugin_id] = $plugin_form;
646 elseif ($item_mode == 'preview') {
647 $element['subform'] = array();
648 $element['behavior_plugins'] = [];
649 $element['preview'] = entity_view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
650 $element['preview']['#access'] = $paragraphs_entity->access('view');
652 elseif ($item_mode == 'closed') {
653 $element['subform'] = array();
654 $element['behavior_plugins'] = [];
655 if ($paragraphs_entity) {
656 $summary = $this->addCollapsedSummary($paragraphs_entity);
657 $element['top']['paragraph_summary']['fields_info'] = [
658 '#markup' => $summary,
659 '#prefix' => '<div class="paragraphs-collapsed-description">',
660 '#suffix' => '</div>',
665 $element['subform'] = array();
668 $element['subform']['#attributes']['class'][] = 'paragraphs-subform';
669 $element['subform']['#access'] = $paragraphs_entity->access('update');
671 if ($item_mode == 'remove') {
672 $element['#access'] = FALSE;
675 $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity;
676 $widget_state['paragraphs'][$delta]['display'] = $display;
677 $widget_state['paragraphs'][$delta]['mode'] = $item_mode;
679 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
682 $element['#access'] = FALSE;
688 public function getAllowedTypes() {
690 $return_bundles = array();
692 $target_type = $this->getFieldSetting('target_type');
693 $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
695 if ($this->getSelectionHandlerSetting('target_bundles') !== NULL) {
696 $bundles = array_intersect_key($bundles, $this->getSelectionHandlerSetting('target_bundles'));
699 // Support for the paragraphs reference type.
700 $drag_drop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
701 if ($drag_drop_settings) {
702 $max_weight = count($bundles);
704 foreach ($drag_drop_settings as $bundle_info) {
705 if (isset($bundle_info['weight']) && $bundle_info['weight'] && $bundle_info['weight'] > $max_weight) {
706 $max_weight = $bundle_info['weight'];
710 // Default weight for new items.
711 $weight = $max_weight + 1;
712 foreach ($bundles as $machine_name => $bundle) {
713 $return_bundles[$machine_name] = array(
714 'label' => $bundle['label'],
715 'weight' => isset($drag_drop_settings[$machine_name]['weight']) ? $drag_drop_settings[$machine_name]['weight'] : $weight,
720 // Support for other reference types.
723 foreach ($bundles as $machine_name => $bundle) {
724 if (!count($this->getSelectionHandlerSetting('target_bundles'))
725 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles'))) {
727 $return_bundles[$machine_name] = array(
728 'label' => $bundle['label'],
737 uasort($return_bundles, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
739 return $return_bundles;
745 public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
746 $field_name = $this->fieldDefinition->getName();
747 $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
748 $this->fieldParents = $form['#parents'];
749 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
751 $max = $field_state['items_count'];
752 $entity_type_manager = \Drupal::entityTypeManager();
754 // Consider adding a default paragraph for new host entities.
755 if ($max == 0 && $items->getEntity()->isNew()) {
756 $default_type = $this->getDefaultParagraphTypeMachineName();
758 // Checking if default_type is not none and if is allowed.
760 // Place the default paragraph.
761 $target_type = $this->getFieldSetting('target_type');
762 $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create([
763 'type' => $default_type,
765 $field_state['selected_bundle'] = $default_type;
766 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
767 $field_state['paragraphs'][0] = [
768 'entity' => $paragraphs_entity,
769 'display' => $display,
771 'original_delta' => 1
774 $field_state['items_count'] = $max;
778 $this->realItemCount = $max;
779 $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
781 $title = $this->fieldDefinition->getLabel();
782 $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
785 $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name)));
786 $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper');
787 $elements['#prefix'] = '<div id="' . $this->fieldWrapperId . '">';
788 $elements['#suffix'] = '</div>';
790 $field_state['ajax_wrapper_id'] = $this->fieldWrapperId;
791 // Persist the widget state so formElement() can access it.
792 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
795 for ($delta = 0; $delta < $max; $delta++) {
797 // Add a new empty item if it doesn't exist yet at this delta.
798 if (!isset($items[$delta])) {
799 $items->appendItem();
802 // For multiple fields, title and description are handled by the wrapping
805 '#title' => $is_multiple ? '' : $title,
806 '#description' => $is_multiple ? '' : $description,
808 $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
811 // Input field for the delta (drag-n-drop reordering).
813 // We name the element '_weight' to avoid clashing with elements
814 // defined by widget.
815 $element['_weight'] = array(
817 '#title' => $this->t('Weight for row @number', array('@number' => $delta + 1)),
818 '#title_display' => 'invisible',
819 // Note: this 'delta' is the FAPI #type 'weight' element's property.
821 '#default_value' => $items[$delta]->_weight ?: $delta,
826 // Access for the top element is set to FALSE only when the paragraph
827 // was removed. A paragraphs that a user can not edit has access on
829 if (isset($element['#access']) && !$element['#access']) {
830 $this->realItemCount--;
833 $elements[$delta] = $element;
839 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
840 $field_state['real_item_count'] = $this->realItemCount;
841 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
843 if ($this->realItemCount > 0) {
845 '#theme' => 'field_multiple_value_form',
846 '#field_name' => $field_name,
847 '#cardinality' => $cardinality,
848 '#cardinality_multiple' => $is_multiple,
849 '#required' => $this->fieldDefinition->isRequired(),
851 '#description' => $description,
852 '#max_delta' => $max-1,
857 '#type' => 'container',
858 '#theme_wrappers' => ['container'],
859 '#field_name' => $field_name,
860 '#cardinality' => $cardinality,
861 '#cardinality_multiple' => TRUE,
862 '#max_delta' => $max-1,
864 '#type' => 'html_tag',
869 '#type' => 'container',
871 '#markup' => $this->t('No @title added yet.', ['@title' => $this->getSetting('title')]),
873 '#suffix' => '</em>',
879 $elements['description'] = [
880 '#type' => 'container',
881 'value' => ['#markup' => $description],
882 '#attributes' => ['class' => ['description']],
887 $host = $items->getEntity();
888 $this->initIsTranslating($form_state, $host);
890 if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) {
891 $elements['add_more'] = $this->buildAddActions();
894 $elements['#attached']['library'][] = 'paragraphs/drupal.paragraphs.widget';
902 public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
903 $parents = $form['#parents'];
905 // Identify the manage field settings default value form.
906 if (in_array('default_value_input', $parents, TRUE)) {
907 // Since the entity is not reusable neither cloneable, having a default
908 // value is not supported.
909 return ['#markup' => $this->t('No widget available for: %label.', ['%label' => $items->getFieldDefinition()->getLabel()])];
912 return parent::form($items, $form, $form_state, $get_delta);
916 * Add 'add more' button, if not working with a programmed form.
919 * The form element array.
921 protected function buildAddActions() {
922 if (count($this->getAccessibleOptions()) === 0) {
923 if (count($this->getAllowedTypes()) === 0) {
924 $add_more_elements['info'] = [
925 '#type' => 'container',
926 '#markup' => $this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]),
927 '#attributes' => ['class' => ['messages', 'messages--warning']],
931 $add_more_elements['info'] = [
932 '#type' => 'container',
933 '#markup' => $this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]),
934 '#attributes' => ['class' => ['messages', 'messages--warning']],
938 return $add_more_elements ;
941 if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') {
942 return $this->buildButtonsAddMode();
945 return $this->buildSelectAddMode();
949 * Returns the available paragraphs type.
952 * Available paragraphs types.
954 protected function getAccessibleOptions() {
955 if ($this->accessOptions !== NULL) {
956 return $this->accessOptions;
959 $entity_type_manager = \Drupal::entityTypeManager();
960 $target_type = $this->getFieldSetting('target_type');
961 $bundles = $this->getAllowedTypes();
962 $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
963 $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
965 foreach ($bundles as $machine_name => $bundle) {
966 if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
967 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
968 if ($access_control_handler->createAccess($machine_name)) {
969 $this->accessOptions[$machine_name] = $bundle['label'];
974 return $this->accessOptions;
978 * Builds dropdown button for adding new paragraph.
981 * The form element array.
983 protected function buildButtonsAddMode() {
984 // Hide the button when translating.
985 $add_more_elements = [
986 '#type' => 'container',
987 '#theme_wrappers' => ['paragraphs_dropbutton_wrapper'],
989 $field_name = $this->fieldDefinition->getName();
990 $title = $this->fieldDefinition->getLabel();
992 $drop_button = FALSE;
993 if (count($this->getAccessibleOptions()) > 1 && $this->getSetting('add_mode') == 'dropdown') {
995 $add_more_elements['#theme_wrappers'] = ['dropbutton_wrapper'];
996 $add_more_elements['prefix'] = [
997 '#markup' => '<ul class="dropbutton">',
1000 $add_more_elements['suffix'] = [
1001 '#markup' => '</ul>',
1004 $add_more_elements['#suffix'] = $this->t(' to %type', ['%type' => $title]);
1007 foreach ($this->getAccessibleOptions() as $machine_name => $label) {
1008 $add_more_elements['add_more_button_' . $machine_name] = [
1009 '#type' => 'submit',
1010 '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_' . $machine_name . '_add_more',
1011 '#value' => $this->t('Add @type', ['@type' => $label]),
1012 '#attributes' => ['class' => ['field-add-more-submit']],
1013 '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1014 '#submit' => [[get_class($this), 'addMoreSubmit']],
1016 'callback' => [get_class($this), 'addMoreAjax'],
1017 'wrapper' => $this->fieldWrapperId,
1020 '#bundle_machine_name' => $machine_name,
1024 $add_more_elements['add_more_button_' . $machine_name]['#prefix'] = '<li>';
1025 $add_more_elements['add_more_button_' . $machine_name]['#suffix'] = '</li>';
1029 return $add_more_elements;
1033 * Builds list of actions based on paragraphs type.
1036 * The form element array.
1038 protected function buildSelectAddMode() {
1039 $field_name = $this->fieldDefinition->getName();
1040 $title = $this->fieldDefinition->getLabel();
1041 $add_more_elements['add_more_select'] = [
1042 '#type' => 'select',
1043 '#options' => $this->getAccessibleOptions(),
1044 '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]),
1045 '#label_display' => 'hidden',
1048 $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]);
1050 if ($this->realItemCount > 0) {
1051 $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]);
1054 $add_more_elements['add_more_button'] = [
1055 '#type' => 'submit',
1056 '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more',
1058 '#attributes' => ['class' => ['field-add-more-submit']],
1059 '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1060 '#submit' => [[get_class($this), 'addMoreSubmit']],
1062 'callback' => [get_class($this), 'addMoreAjax'],
1063 'wrapper' => $this->fieldWrapperId,
1068 $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]);
1069 return $add_more_elements;
1075 public static function addMoreAjax(array $form, FormStateInterface $form_state) {
1076 $button = $form_state->getTriggeringElement();
1077 // Go one level up in the form, to the widgets container.
1078 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
1080 // Add a DIV around the delta receiving the Ajax effect.
1081 $delta = $element['#max_delta'];
1082 $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
1083 $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
1091 public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
1092 $button = $form_state->getTriggeringElement();
1094 // Go one level up in the form, to the widgets container.
1095 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
1096 $field_name = $element['#field_name'];
1097 $parents = $element['#field_parents'];
1099 // Increment the items count.
1100 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1102 if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
1103 $widget_state['items_count']++;
1106 if (isset($button['#bundle_machine_name'])) {
1107 $widget_state['selected_bundle'] = $button['#bundle_machine_name'];
1110 $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value'];
1113 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1115 $form_state->setRebuild();
1119 * Creates a duplicate of the paragraph entity.
1121 public static function duplicateSubmit(array $form, FormStateInterface $form_state) {
1122 $button = $form_state->getTriggeringElement();
1123 // Go one level up in the form, to the widgets container.
1124 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
1125 $field_name = $element['#field_name'];
1126 $parents = $element['#field_parents'];
1128 // Inserting new element in the array.
1129 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1130 $delta = $button['#delta'];
1131 $widget_state['items_count']++;
1132 $widget_state['real_item_count']++;
1133 $widget_state['original_deltas'] = array_merge($widget_state['original_deltas'], ['1' => 1]) ;
1135 // Check if the replicate module is enabled
1136 if (\Drupal::hasService('replicate.replicator')) {
1137 $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($widget_state['paragraphs'][$delta]['entity']);
1140 $duplicate_entity = $widget_state['paragraphs'][$delta]['entity']->createDuplicate();
1142 // Create the duplicated paragraph and insert it below the original.
1144 'entity' => $duplicate_entity,
1145 'display' => $widget_state['paragraphs'][$delta]['display'],
1149 array_splice($widget_state['paragraphs'], $delta + 1, 0, $paragraph);
1151 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1152 $form_state->setRebuild();
1155 public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
1156 $button = $form_state->getTriggeringElement();
1158 // Go one level up in the form, to the widgets container.
1159 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
1161 $delta = array_slice($button['#array_parents'], -4, -3);
1164 $field_name = $element['#field_name'];
1165 $parents = $element['#field_parents'];
1167 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1169 $widget_state['paragraphs'][$delta]['mode'] = $button['#paragraphs_mode'];
1171 if (!empty($button['#paragraphs_show_warning'])) {
1172 $widget_state['paragraphs'][$delta]['show_warning'] = $button['#paragraphs_show_warning'];
1175 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1177 $form_state->setRebuild();
1180 public static function itemAjax(array $form, FormStateInterface $form_state) {
1181 $button = $form_state->getTriggeringElement();
1182 // Go one level up in the form, to the widgets container.
1183 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
1185 $element['#prefix'] = '<div class="ajax-new-content">' . (isset($element['#prefix']) ? $element['#prefix'] : '');
1186 $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>';
1192 * Returns the value of a setting for the entity reference selection handler.
1194 * @param string $setting_name
1198 * The setting value.
1200 protected function getSelectionHandlerSetting($setting_name) {
1201 $settings = $this->getFieldSetting('handler_settings');
1202 return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
1208 public function elementValidate($element, FormStateInterface $form_state, $form) {
1209 $field_name = $this->fieldDefinition->getName();
1210 $widget_state = static::getWidgetState($element['#field_parents'], $field_name, $form_state);
1211 $delta = $element['#delta'];
1213 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
1214 $entity = $widget_state['paragraphs'][$delta]['entity'];
1216 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1217 $display = $widget_state['paragraphs'][$delta]['display'];
1219 if ($widget_state['paragraphs'][$delta]['mode'] == 'edit') {
1220 // Extract the form values on submit for getting the current paragraph.
1221 $display->extractFormValues($entity, $element['subform'], $form_state);
1222 $display->validateFormValues($entity, $element['subform'], $form_state);
1224 // Validate all enabled behavior plugins.
1225 $paragraphs_type = $entity->getParagraphType();
1226 foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
1227 $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
1228 $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state);
1233 static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state);
1239 public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
1240 $entity = $form_state->getFormObject()->getEntity();
1241 $field_name = $this->fieldDefinition->getName();
1242 $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1243 $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']);
1245 $new_revision = FALSE;
1246 if ($entity instanceof RevisionableInterface) {
1247 if ($entity->isNewRevision()) {
1248 $new_revision = TRUE;
1250 // Most of the time we don't know yet if the host entity is going to be
1251 // saved as a new revision using RevisionableInterface::isNewRevision().
1252 // Most entity types (at least nodes) however use a boolean property named
1253 // "revision" to indicate whether a new revision should be saved. Use that
1255 elseif ($entity->getEntityType()->hasKey('revision') && $form_state->getValue('revision')) {
1256 $new_revision = TRUE;
1260 foreach ($values as $delta => &$item) {
1261 if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
1262 && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
1263 $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity'];
1265 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1266 $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
1267 if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') {
1268 $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state);
1270 $paragraphs_entity->setNewRevision($new_revision);
1271 // A content entity form saves without any rebuild. It needs to set the
1272 // language to update it in case of language change.
1273 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
1274 if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) {
1275 // If a translation in the given language already exists, switch to
1276 // that. If there is none yet, update the language.
1277 if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) {
1278 $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode'));
1281 $paragraphs_entity->set($langcode_key, $form_state->get('langcode'));
1284 if (isset($item['behavior_plugins'])) {
1285 // Submit all enabled behavior plugins.
1286 $paragraphs_type = $paragraphs_entity->getParagraphType();
1287 foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
1288 if (!isset($item['behavior_plugins'][$plugin_id])) {
1289 $item['behavior_plugins'][$plugin_id] = [];
1291 if (isset($element[$delta]) && isset($element[$delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm()) {
1292 $subform_state = SubformState::createForSubform($element[$delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
1293 $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state);
1298 $paragraphs_entity->setNeedsSave(TRUE);
1299 $item['entity'] = $paragraphs_entity;
1300 $item['target_id'] = $paragraphs_entity->id();
1301 $item['target_revision_id'] = $paragraphs_entity->getRevisionId();
1303 // If our mode is remove don't save or reference this entity.
1304 // @todo: Maybe we should actually delete it here?
1305 elseif($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') {
1306 $item['target_id'] = NULL;
1307 $item['target_revision_id'] = NULL;
1316 public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
1317 // Filter possible empty items.
1318 $items->filterEmptyItems();
1319 return parent::extractFormValues($items, $form, $form_state);
1323 * Initializes the translation form state.
1325 * @param \Drupal\Core\Form\FormStateInterface $form_state
1326 * @param \Drupal\Core\Entity\EntityInterface $host
1328 protected function initIsTranslating(FormStateInterface $form_state, EntityInterface $host) {
1329 if ($this->isTranslating != NULL) {
1332 $this->isTranslating = FALSE;
1333 if (!$host->isTranslatable()) {
1336 if (!$host->getEntityType()->hasKey('default_langcode')) {
1339 $default_langcode_key = $host->getEntityType()->getKey('default_langcode');
1340 if (!$host->hasField($default_langcode_key)) {
1344 if (!empty($form_state->get('content_translation'))) {
1345 // Adding a language through the ContentTranslationController.
1346 $this->isTranslating = TRUE;
1348 if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) {
1349 // Editing a translation.
1350 $this->isTranslating = TRUE;
1355 * After-build callback for removing the translatability clue from the widget.
1357 * If the fields on the paragraph type are translatable,
1358 * ContentTranslationHandler::addTranslatabilityClue()adds an
1359 * "(all languages)" suffix to the widget title. That suffix is incorrect and
1360 * is being removed by this method using a #after_build on the field widget.
1362 * @param array $element
1363 * @param \Drupal\Core\Form\FormStateInterface $form_state
1367 public static function removeTranslatabilityClue(array $element, FormStateInterface $form_state) {
1368 // Widgets could have multiple elements with their own titles, so remove the
1369 // suffix if it exists, do not recurse lower than this to avoid going into
1370 // nested paragraphs or similar nested field types.
1371 $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>';
1372 if (isset($element['#title']) && strpos($element['#title'], $suffix)) {
1373 $element['#title'] = str_replace($suffix, '', $element['#title']);
1375 // Loop over all widget deltas.
1376 foreach (Element::children($element) as $delta) {
1377 if (isset($element[$delta]['#title']) && strpos($element[$delta]['#title'], $suffix)) {
1378 $element[$delta]['#title'] = str_replace($suffix, '', $element[$delta]['#title']);
1380 // Loop over all form elements within the current delta.
1381 foreach (Element::children($element[$delta]) as $field) {
1382 if (isset($element[$delta][$field]['#title']) && strpos($element[$delta][$field]['#title'], $suffix)) {
1383 $element[$delta][$field]['#title'] = str_replace($suffix, '', $element[$delta][$field]['#title']);
1391 * Returns the default paragraph type.
1393 * @return string $default_paragraph_type
1394 * Label name for default paragraph type.
1396 protected function getDefaultParagraphTypeLabelName(){
1397 if ($this->getDefaultParagraphTypeMachineName() !== NULL) {
1398 $allowed_types = $this->getAllowedTypes();
1399 return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label'];
1406 * Returns the machine name for default paragraph type.
1409 * Machine name for default paragraph type.
1411 protected function getDefaultParagraphTypeMachineName() {
1412 $default_type = $this->getSetting('default_paragraph_type');
1413 $allowed_types = $this->getAllowedTypes();
1414 if ($default_type && isset($allowed_types[$default_type])) {
1415 return $default_type;
1417 // Check if the user explicitly selected not to have any default Paragraph
1418 // type. Othewise, if there is only one type available, that one is the
1420 if ($default_type === '_none') {
1423 if (count($allowed_types) === 1) {
1424 return key($allowed_types);
1431 * @param \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity
1432 * Entity where to extract the values.
1434 * @return string $collapsed_summary_text
1435 * The text without tags to return.
1437 public function addCollapsedSummary(paragraphs\Entity\Paragraph $paragraphs_entity) {
1438 $text_types = ['text_with_summary', 'text', 'text_long', 'list_string'];
1440 foreach ($paragraphs_entity->getFieldDefinitions() as $key => $value) {
1441 if ($value->getType() == 'image') {
1442 if ($paragraphs_entity->get($key)->entity) {
1443 foreach ($paragraphs_entity->get($key) as $image_key => $image_value) {
1444 if ($image_value->title != '') {
1445 $text = $image_value->title;
1447 elseif ($image_value->alt != '') {
1448 $text = $image_value->alt;
1450 elseif ($text = $image_value->entity->filename->value) {
1451 $text = $image_value->entity->filename->value;
1453 if (strlen($text) > 50) {
1454 $text = strip_tags(substr($text, 0, 150));
1460 if (in_array($value->getType(), $text_types)) {
1461 $text = $paragraphs_entity->get($key)->value;
1462 if (strlen($text) > 50) {
1463 $text = strip_tags(substr($text, 0, 150));
1467 if ($field_type = $value->getType() == 'entity_reference_revisions') {
1468 if ($paragraphs_entity->get($key) && $paragraphs_entity->get($key)->entity) {
1469 $summary[] = $this->addCollapsedSummary($paragraphs_entity->get($key)->entity);
1472 if ($field_type = $value->getType() == 'entity_reference') {
1473 if (!in_array($key, ['type', 'uid', 'revision_uid'])) {
1474 if ($paragraphs_entity->get($key)->entity) {
1475 $summary[] = $paragraphs_entity->get($key)->entity->label();
1480 $paragraphs_type = $paragraphs_entity->getParagraphType();
1481 foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
1482 if ($plugin_summary = $plugin->settingsSummary($paragraphs_entity)) {
1483 $summary = array_merge($summary, $plugin_summary);
1486 $collapsed_summary_text = implode(', ', $summary);
1487 return strip_tags($collapsed_summary_text);
1493 public static function isApplicable(FieldDefinitionInterface $field_definition) {
1494 $target_type = $field_definition->getSetting('target_type');
1495 $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type);
1496 if ($paragraph_type) {
1497 return $paragraph_type->isSubclassOf(ParagraphInterface::class);