3 namespace Drupal\paragraphs\Plugin\Field\FieldWidget;
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Core\Entity\ContentEntityInterface;
8 use Drupal\Core\Entity\Entity\EntityFormDisplay;
9 use Drupal\Core\Entity\FieldableEntityInterface;
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\ParagraphInterface;
19 use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection;
20 use Symfony\Component\Validator\ConstraintViolationListInterface;
23 * Plugin implementation of the 'entity_reference_revisions paragraphs' widget.
27 * label = @Translation("Paragraphs EXPERIMENTAL"),
28 * description = @Translation("An experimental paragraphs inline form widget."),
30 * "entity_reference_revisions"
34 class ParagraphsWidget extends WidgetBase {
37 * Action position is in the add paragraphs place.
39 const ACTION_POSITION_BASE = 1;
42 * Action position is in the table header section.
44 const ACTION_POSITION_HEADER = 2;
47 * Action position is in the actions section of the widget.
49 const ACTION_POSITION_ACTIONS = 3;
52 * Indicates whether the current widget instance is in translation.
56 protected $isTranslating;
59 * Id to name ajax buttons that includes field parents and field name.
63 protected $fieldIdPrefix;
66 * Wrapper id to identify the paragraphs.
70 protected $fieldWrapperId;
73 * Number of paragraphs item on form.
77 protected $realItemCount;
80 * Parents for the current paragraph.
84 protected $fieldParents;
87 * Accessible paragraphs types.
91 protected $accessOptions = NULL;
94 * Constructs a ParagraphsWidget object.
96 * @param string $plugin_id
97 * The plugin_id for the widget.
98 * @param mixed $plugin_definition
99 * The plugin implementation definition.
100 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
101 * The definition of the field to which the widget is associated.
102 * @param array $settings
103 * The widget settings.
104 * @param array $third_party_settings
105 * Any third party settings.
107 public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
108 // Modify settings that were set before https://www.drupal.org/node/2896115.
109 if(isset($settings['edit_mode']) && $settings['edit_mode'] === 'preview') {
110 $settings['edit_mode'] = 'closed';
111 $settings['closed_mode'] = 'preview';
114 parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
120 public static function defaultSettings() {
122 'title' => t('Paragraph'),
123 'title_plural' => t('Paragraphs'),
124 'edit_mode' => 'open',
125 'closed_mode' => 'summary',
126 'autocollapse' => 'none',
127 'add_mode' => 'dropdown',
128 'form_display_mode' => 'default',
129 'default_paragraph_type' => '',
136 public function settingsForm(array $form, FormStateInterface $form_state) {
139 $elements['title'] = array(
140 '#type' => 'textfield',
141 '#title' => $this->t('Paragraph Title'),
142 '#description' => $this->t('Label to appear as title on the button as "Add new [title]", this label is translatable'),
143 '#default_value' => $this->getSetting('title'),
147 $elements['title_plural'] = array(
148 '#type' => 'textfield',
149 '#title' => $this->t('Plural Paragraph Title'),
150 '#description' => $this->t('Title in its plural form.'),
151 '#default_value' => $this->getSetting('title_plural'),
155 $elements['edit_mode'] = array(
157 '#title' => $this->t('Edit mode'),
158 '#description' => $this->t('The mode the paragraph is in by default.'),
159 '#options' => $this->getSettingOptions('edit_mode'),
160 '#default_value' => $this->getSetting('edit_mode'),
164 $elements['closed_mode'] = [
166 '#title' => $this->t('Closed mode'),
167 '#description' => $this->t('How to display the paragraphs, when the widget is closed. Preview will render the paragraph in the preview view mode and typically needs a custom admin theme.'),
168 '#options' => $this->getSettingOptions('closed_mode'),
169 '#default_value' => $this->getSetting('closed_mode'),
173 $elements['autocollapse'] = [
175 '#title' => $this->t('Autocollapse'),
176 '#description' => $this->t('When a paragraph is opened for editing, close others.'),
177 '#options' => $this->getSettingOptions('autocollapse'),
178 '#default_value' => $this->getSetting('autocollapse'),
182 $elements['add_mode'] = array(
184 '#title' => $this->t('Add mode'),
185 '#description' => $this->t('The way to add new Paragraphs.'),
186 '#options' => $this->getSettingOptions('add_mode'),
187 '#default_value' => $this->getSetting('add_mode'),
191 $elements['form_display_mode'] = array(
193 '#options' => \Drupal::service('entity_display.repository')->getFormModeOptions($this->getFieldSetting('target_type')),
194 '#description' => $this->t('The form display mode to use when rendering the paragraph form.'),
195 '#title' => $this->t('Form display mode'),
196 '#default_value' => $this->getSetting('form_display_mode'),
201 foreach ($this->getAllowedTypes() as $key => $bundle) {
202 $options[$key] = $bundle['label'];
205 $elements['default_paragraph_type'] = [
207 '#title' => $this->t('Default paragraph type'),
208 '#empty_value' => '_none',
209 '#default_value' => $this->getDefaultParagraphTypeMachineName(),
210 '#options' => $options,
211 '#description' => $this->t('When creating a new host entity, a paragraph of this type is added.'),
218 * Returns select options for a plugin setting.
220 * This is done to allow
221 * \Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget::settingsSummary()
222 * to access option labels. Not all plugin setting are available.
224 * @param string $setting_name
225 * The name of the widget setting. Supported settings:
232 * An array of setting option usable as a value for a "#options" key.
234 protected function getSettingOptions($setting_name) {
235 switch($setting_name) {
238 'open' => $this->t('Open'),
239 'closed' => $this->t('Closed'),
244 'summary' => $this->t('Summary'),
245 'preview' => $this->t('Preview'),
250 'none' => $this->t('None'),
251 'all' => $this->t('All'),
256 'select' => $this->t('Select list'),
257 'button' => $this->t('Buttons'),
258 'dropdown' => $this->t('Dropdown button'),
259 'modal' => $this->t('Modal form'),
264 return isset($options) ? $options : NULL;
270 public function settingsSummary() {
272 $summary[] = $this->t('Title: @title', ['@title' => $this->getSetting('title')]);
273 $summary[] = $this->t('Plural title: @title_plural', [
274 '@title_plural' => $this->getSetting('title_plural')
277 $edit_mode = $this->getSettingOptions('edit_mode')[$this->getSetting('edit_mode')];
278 $closed_mode = $this->getSettingOptions('closed_mode')[$this->getSetting('closed_mode')];
279 $autocollapse = $this->getSettingOptions('autocollapse')[$this->getSetting('autocollapse')];
280 $add_mode = $this->getSettingOptions('add_mode')[$this->getSetting('add_mode')];
282 $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]);
283 $summary[] = $this->t('Closed mode: @closed_mode', ['@closed_mode' => $closed_mode]);
284 $summary[] = $this->t('Autocollapse: @autocollapse', ['@autocollapse' => $autocollapse]);
285 $summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]);
287 $summary[] = $this->t('Form display mode: @form_display_mode', [
288 '@form_display_mode' => $this->getSetting('form_display_mode')
290 if ($this->getDefaultParagraphTypeLabelName() !== NULL) {
291 $summary[] = $this->t('Default paragraph type: @default_paragraph_type', [
292 '@default_paragraph_type' => $this->getDefaultParagraphTypeLabelName()
302 * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation()
303 * Uses a similar approach to populate a new translation.
305 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
306 $field_name = $this->fieldDefinition->getName();
307 $parents = $element['#field_parents'];
310 /** @var \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity */
311 $paragraphs_entity = NULL;
312 $host = $items->getEntity();
313 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
315 $entity_type_manager = \Drupal::entityTypeManager();
316 $target_type = $this->getFieldSetting('target_type');
318 $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit';
319 $default_edit_mode = $this->getSetting('edit_mode');
321 $closed_mode_setting = isset($widget_state['closed_mode']) ? $widget_state['closed_mode'] : $this->getSetting('closed_mode');
322 $autocollapse_setting = isset($widget_state['autocollapse']) ? $widget_state['autocollapse'] : $this->getSetting('autocollapse');
324 $show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']);
326 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
327 $paragraphs_entity = $widget_state['paragraphs'][$delta]['entity'];
329 elseif (isset($items[$delta]->entity)) {
330 $paragraphs_entity = $items[$delta]->entity;
332 // We don't have a widget state yet, get from selector settings.
333 if (!isset($widget_state['paragraphs'][$delta]['mode'])) {
335 if ($default_edit_mode == 'open') {
338 elseif ($default_edit_mode == 'closed') {
339 $item_mode = 'closed';
343 elseif (isset($widget_state['selected_bundle'])) {
345 $entity_type = $entity_type_manager->getDefinition($target_type);
346 $bundle_key = $entity_type->getKey('bundle');
348 $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create(array(
349 $bundle_key => $widget_state['selected_bundle'],
355 if ($item_mode == 'closed') {
356 // Validate closed paragraphs and expand if needed.
357 // @todo Consider recursion.
358 $violations = $paragraphs_entity->validate();
359 $violations->filterByFieldAccess();
360 if (count($violations) > 0) {
363 foreach ($violations as $violation) {
364 $messages[] = $violation->getMessage();
366 $info['validation_error'] = $this->createMessage($this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]));
370 if ($paragraphs_entity) {
371 // Detect if we are translating.
372 $this->initIsTranslating($form_state, $host);
373 $langcode = $form_state->get('langcode');
375 if (!$this->isTranslating) {
376 // Set the langcode if we are not translating.
377 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
378 if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
379 // If a translation in the given language already exists, switch to
380 // that. If there is none yet, update the language.
381 if ($paragraphs_entity->hasTranslation($langcode)) {
382 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
385 $paragraphs_entity->set($langcode_key, $langcode);
390 // Add translation if missing for the target language.
391 if (!$paragraphs_entity->hasTranslation($langcode)) {
392 // Get the selected translation of the paragraph entity.
393 $entity_langcode = $paragraphs_entity->language()->getId();
394 $source = $form_state->get(['content_translation', 'source']);
395 $source_langcode = $source ? $source->getId() : $entity_langcode;
396 // Make sure the source language version is used if available. It is a
397 // the host and fetching the translation without this check could lead
398 // valid scenario to have no paragraphs items in the source version of
400 if ($paragraphs_entity->hasTranslation($source_langcode)) {
401 $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
403 // The paragraphs entity has no content translation source field if
404 // no paragraph entity field is translatable, even if the host is.
405 if ($paragraphs_entity->hasField('content_translation_source')) {
406 // Initialise the translation with source language values.
407 $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray());
408 $translation = $paragraphs_entity->getTranslation($langcode);
409 $manager = \Drupal::service('content_translation.manager');
410 $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId());
413 // If any paragraphs type is translatable do not switch.
414 if ($paragraphs_entity->hasField('content_translation_source')) {
415 // Switch the paragraph to the translation.
416 $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
420 $element_parents = $parents;
421 $element_parents[] = $field_name;
422 $element_parents[] = $delta;
423 $element_parents[] = 'subform';
425 $id_prefix = implode('-', array_merge($parents, array($field_name, $delta)));
426 $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper');
429 '#type' => 'container',
430 '#element_validate' => array(array($this, 'elementValidate')),
432 '#type' => 'container',
433 '#parents' => $element_parents,
437 $element['#prefix'] = '<div id="' . $wrapper_id . '">';
438 $element['#suffix'] = '</div>';
440 $item_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
441 if (isset($item_bundles[$paragraphs_entity->bundle()])) {
442 $bundle_info = $item_bundles[$paragraphs_entity->bundle()];
444 // Create top section structure with all needed subsections.
446 '#type' => 'container',
448 '#attributes' => ['class' => ['paragraph-type-top']],
449 // Section for paragraph type information.
451 '#type' => 'container',
452 '#attributes' => ['class' => ['paragraph-type-title']],
453 'label' => ['#markup' => $bundle_info['label']],
455 // Section for information icons.
457 '#type' => 'container',
458 '#attributes' => ['class' => ['paragraph-type-info']],
461 '#type' => 'container',
462 '#attributes' => ['class' => ['paragraph-type-summary']],
464 // Paragraphs actions element for actions and dropdown actions.
466 '#type' => 'paragraphs_actions',
470 // Type icon and label bundle.
471 if ($icon_url = $paragraphs_entity->type->entity->getIconUrl()) {
472 $element['top']['type']['icon'] = [
476 'class' => ['paragraph-type-icon'],
477 'title' => $bundle_info['label'],
480 // We set inline height and width so icon don't resize on first load
481 // while CSS is still not loaded.
486 $element['top']['type']['label'] = [
487 '#markup' => '<span class="paragraph-type-label">' . $bundle_info['label'] . '</span>',
494 'dropdown_actions' => [],
497 $widget_actions['dropdown_actions']['duplicate_button'] = [
499 '#value' => $this->t('Duplicate'),
500 '#name' => $id_prefix . '_duplicate',
502 '#submit' => [[get_class($this), 'duplicateSubmit']],
503 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
506 'callback' => [get_class($this), 'itemAjax'],
507 'wrapper' => $widget_state['ajax_wrapper_id'],
509 '#access' => $paragraphs_entity->access('update'),
512 if ($item_mode != 'remove') {
513 $widget_actions['dropdown_actions']['remove_button'] = [
515 '#value' => $this->t('Remove'),
516 '#name' => $id_prefix . '_remove',
518 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
519 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
522 'callback' => array(get_class($this), 'itemAjax'),
523 'wrapper' => $widget_state['ajax_wrapper_id'],
525 // Hide the button when translating.
526 '#access' => $paragraphs_entity->access('delete') && !$this->isTranslating,
527 '#paragraphs_mode' => 'remove',
531 if ($item_mode == 'edit') {
532 if (isset($paragraphs_entity)) {
533 $widget_actions['actions']['collapse_button'] = [
534 '#value' => $this->t('Collapse'),
535 '#name' => $id_prefix . '_collapse',
537 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
538 '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
541 'callback' => [get_class($this), 'itemAjax'],
542 'wrapper' => $widget_state['ajax_wrapper_id'],
544 '#access' => $paragraphs_entity->access('update'),
545 '#paragraphs_mode' => 'closed',
546 '#paragraphs_show_warning' => TRUE,
548 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
549 'title' => $this->t('Collapse'),
555 $widget_actions['actions']['edit_button'] = $this->expandButton([
557 '#value' => $this->t('Edit'),
558 '#name' => $id_prefix . '_edit',
560 '#attributes' => ['class' => ['paragraphs-button']],
561 '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
562 '#limit_validation_errors' => [
563 array_merge($parents, [$field_name, 'add_more']),
567 'callback' => [get_class($this), 'itemAjax'],
568 'wrapper' => $widget_state['ajax_wrapper_id'],
570 '#access' => $paragraphs_entity->access('update'),
571 '#paragraphs_mode' => 'edit',
573 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
574 'title' => $this->t('Edit'),
578 if ($show_must_be_saved_warning && $paragraphs_entity->isChanged()) {
580 '#theme' => 'paragraphs_info_icon',
581 '#message' => $this->t('You have unsaved changes on this @title item.', ['@title' => $this->getSetting('title')]),
582 '#icon' => 'changed',
586 if (!$paragraphs_entity->access('view')) {
588 '#theme' => 'paragraphs_info_icon',
589 '#message' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
595 // If update is disabled we will show lock icon in actions section.
596 if (!$paragraphs_entity->access('update')) {
597 $widget_actions['actions']['edit_disabled'] = [
598 '#theme' => 'paragraphs_info_icon',
599 '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
605 if (!$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete')) {
607 '#theme' => 'paragraphs_info_icon',
608 '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
612 elseif (!$paragraphs_entity->access('update')) {
614 '#theme' => 'paragraphs_info_icon',
615 '#message' => $this->t('You are not allowed to edit this @title.', ['@title' => $this->getSetting('title')]),
616 '#icon' => 'edit-disabled',
619 elseif (!$paragraphs_entity->access('delete')) {
621 '#theme' => 'paragraphs_info_icon',
622 '#message' => $this->t('You are not allowed to remove this @title.', ['@title' => $this->getSetting('title')]),
623 '#icon' => 'delete-disabled',
629 'widget' => self::getWidgetState($parents, $field_name, $form_state, $widget_state),
632 'element' => $element,
633 'form_state' => $form_state,
634 'paragraphs_entity' => $paragraphs_entity,
637 // Allow modules to alter widget actions.
638 \Drupal::moduleHandler()->alter('paragraphs_widget_actions', $widget_actions, $context);
640 if (count($widget_actions['actions'])) {
641 // Expand all actions to proper submit elements and add it to top
642 // actions sub component.
643 $element['top']['actions']['actions'] = array_map([$this, 'expandButton'], $widget_actions['actions']);
646 if (count($widget_actions['dropdown_actions'])) {
647 // Expand all dropdown actions to proper submit elements and add
648 // them to top dropdown actions sub component.
649 $element['top']['actions']['dropdown_actions'] = array_map([$this, 'expandButton'], $widget_actions['dropdown_actions']);
653 foreach ($info as $info_item) {
654 if (!isset($info_item['#access']) || $info_item['#access']) {
655 $element['top']['info']['items'] = $info;
662 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
664 // @todo Remove as part of https://www.drupal.org/node/2640056
665 if (\Drupal::moduleHandler()->moduleExists('field_group')) {
667 'entity_type' => $paragraphs_entity->getEntityTypeId(),
668 'bundle' => $paragraphs_entity->bundle(),
669 'entity' => $paragraphs_entity,
671 'display_context' => 'form',
672 'mode' => $display->getMode(),
675 field_group_attach_groups($element['subform'], $context);
676 $element['subform']['#pre_render'][] = 'field_group_form_pre_render';
679 if ($item_mode == 'edit') {
680 $display->buildForm($paragraphs_entity, $element['subform'], $form_state);
681 // Get the field definitions of the paragraphs_entity.
682 // We need them to filter out entity reference revisions fields that
683 // reference paragraphs, cause otherwise we have problems with showing
684 // and hiding the right fields in nested paragraphs.
685 $field_definitions = $paragraphs_entity->getFieldDefinitions();
687 foreach (Element::children($element['subform']) as $field) {
688 // Do a check if we have to add a class to the form element. We need
689 // those classes (paragraphs-content and paragraphs-behavior) to show
690 // and hide elements, depending of the active perspective.
692 if (isset($field_definitions[$field])) {
693 $type = $field_definitions[$field]->getType();
694 if ($type == 'entity_reference_revisions') {
695 // Check if we are referencing paragraphs.
696 $target_entity_type = $field_definitions[$field]->get('entity_type');
697 if ($target_entity_type && $target_entity_type == 'paragraph') {
703 if ($paragraphs_entity->hasField($field)) {
705 $element['subform'][$field]['#attributes']['class'][] = 'paragraphs-content';
707 $translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable();
709 $element['subform'][$field]['widget']['#after_build'][] = [
711 'removeTranslatabilityClue',
717 // Build the behavior plugins fields.
718 $paragraphs_type = $paragraphs_entity->getParagraphType();
719 if ($paragraphs_type && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
720 $element['behavior_plugins']['#weight'] = -99;
721 foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
722 $element['behavior_plugins'][$plugin_id] = [
723 '#type' => 'container',
724 '#group' => implode('][', array_merge($element_parents, ['paragraph_behavior'])),
726 $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form, $form_state);
727 if ($plugin_form = $plugin->buildBehaviorForm($paragraphs_entity, $element['behavior_plugins'][$plugin_id], $subform_state)) {
728 $element['behavior_plugins'][$plugin_id] = $plugin_form;
729 // Add the paragraphs-behavior class, so that we are able to show
730 // and hide behavior fields, depending on the active perspective.
731 $element['behavior_plugins'][$plugin_id]['#attributes']['class'][] = 'paragraphs-behavior';
736 elseif ($item_mode == 'closed') {
737 $element['subform'] = [];
738 $element['behavior_plugins'] = [];
739 if ($closed_mode_setting === 'preview') {
740 // The closed paragraph is displayed as a rendered preview.
741 $view_builder = $entity_type_manager->getViewBuilder('paragraph');
743 $element['preview'] = $view_builder->view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
744 $element['preview']['#access'] = $paragraphs_entity->access('view');
747 // The closed paragraph is displayed as a summary.
748 if ($paragraphs_entity) {
749 $summary = $paragraphs_entity->getSummary();
750 if (!empty($summary)) {
751 $element['top']['summary']['fields_info'] = [
752 '#markup' => $summary,
753 '#prefix' => '<div class="paragraphs-collapsed-description">',
754 '#suffix' => '</div>',
755 '#access' => $paragraphs_entity->access('view'),
762 $element['subform'] = array();
765 $element['subform']['#attributes']['class'][] = 'paragraphs-subform';
766 $element['subform']['#access'] = $paragraphs_entity->access('update');
768 if ($item_mode == 'remove') {
769 $element['#access'] = FALSE;
772 $widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity;
773 $widget_state['paragraphs'][$delta]['display'] = $display;
774 $widget_state['paragraphs'][$delta]['mode'] = $item_mode;
775 $widget_state['closed_mode'] = $closed_mode_setting;
776 $widget_state['autocollapse'] = $autocollapse_setting;
778 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
781 $element['#access'] = FALSE;
788 * Builds an add paragraph button for opening of modal form.
790 * @param array $element
793 protected function buildModalAddForm(array &$element) {
794 // Attach the theme for the dialog template.
795 $element['#theme'] = 'paragraphs_add_dialog';
797 $element['add_modal_form_area'] = [
798 '#type' => 'container',
801 'paragraph-type-add-modal',
805 '#access' => !$this->isTranslating,
809 $element['add_modal_form_area']['add_more'] = [
811 '#value' => $this->t('Add @title', ['@title' => $this->getSetting('title')]),
812 '#name' => 'button_add_modal',
815 'paragraph-type-add-modal-button',
821 $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.modal';
825 * Returns the sorted allowed types for a entity reference field.
827 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
828 * (optional) The field definition forwhich the allowed types should be
829 * returned, defaults to the current field.
832 * A list of arrays keyed by the paragraph type machine name with the following properties.
833 * - label: The label of the paragraph type.
834 * - weight: The weight of the paragraph type.
836 public function getAllowedTypes(FieldDefinitionInterface $field_definition = NULL) {
838 $return_bundles = array();
839 /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
840 $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
841 $handler = $selection_manager->getSelectionHandler($field_definition ?: $this->fieldDefinition);
842 if ($handler instanceof ParagraphSelection) {
843 $return_bundles = $handler->getSortedAllowedTypes();
845 // Support for other reference types.
847 $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($field_definition ? $field_definition->getSetting('target_type') : $this->fieldDefinition->getSetting('target_type'));
849 foreach ($bundles as $machine_name => $bundle) {
850 if (!count($this->getSelectionHandlerSetting('target_bundles'))
851 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles'))) {
853 $return_bundles[$machine_name] = array(
854 'label' => $bundle['label'],
864 return $return_bundles;
870 public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
871 $field_name = $this->fieldDefinition->getName();
872 $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
873 $this->fieldParents = $form['#parents'];
874 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
876 $max = $field_state['items_count'];
877 $entity_type_manager = \Drupal::entityTypeManager();
879 // Consider adding a default paragraph for new host entities.
880 if ($max == 0 && $items->getEntity()->isNew()) {
881 $default_type = $this->getDefaultParagraphTypeMachineName();
883 // Checking if default_type is not none and if is allowed.
885 // Place the default paragraph.
886 $target_type = $this->getFieldSetting('target_type');
887 $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create([
888 'type' => $default_type,
890 $field_state['selected_bundle'] = $default_type;
891 $display = EntityFormDisplay::collectRenderDisplay($paragraphs_entity, $this->getSetting('form_display_mode'));
892 $field_state['paragraphs'][0] = [
893 'entity' => $paragraphs_entity,
894 'display' => $display,
896 'original_delta' => 1
899 $field_state['items_count'] = $max;
903 $this->realItemCount = $max;
904 $is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
906 $field_title = $this->fieldDefinition->getLabel();
907 $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
911 $this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name)));
912 $this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper');
914 // If the parent entity is paragraph add the nested class if not then add
915 // the perspective tabs.
916 $field_prefix = strtr($this->fieldIdPrefix, '_', '-');
917 if (count($this->fieldParents) == 0) {
918 if ($items->getEntity()->getEntityTypeId() != 'paragraph') {
919 $tabs = '<ul class="paragraphs-tabs tabs primary clearfix"><li id="content" class="tabs__tab"><a href="#' . $field_prefix . '-values">Content</a></li><li id="behavior" class="tabs__tab"><a href="#' . $field_prefix . '-values">Behavior</a></li></ul>';
922 if (count($this->fieldParents) > 0) {
923 if ($items->getEntity()->getEntityTypeId() === 'paragraph') {
924 $form['#attributes']['class'][] = 'paragraphs-nested';
927 $elements['#prefix'] = '<div class="is-horizontal paragraphs-tabs-wrapper" id="' . $this->fieldWrapperId . '">' . $tabs;
928 $elements['#suffix'] = '</div>';
930 $field_state['ajax_wrapper_id'] = $this->fieldWrapperId;
931 // Persist the widget state so formElement() can access it.
932 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
934 $header_actions = $this->buildHeaderActions($field_state, $form_state);
935 if ($header_actions) {
936 $elements['header_actions'] = $header_actions;
937 // Add a weight element so we guaranty that header actions will stay in
938 // first row. We will use this later in
939 // paragraphs_preprocess_field_multiple_value_form().
940 $elements['header_actions']['_weight'] = [
942 '#default_value' => -100,
946 if (!empty($field_state['dragdrop'])) {
947 $elements['#attached']['library'][] = 'paragraphs/paragraphs-dragdrop';
948 //$elements['dragdrop_mode']['#button_type'] = 'primary';
949 $elements['dragdrop'] = $this->buildNestedParagraphsFoDragDrop($form_state, NULL, []);
954 for ($delta = 0; $delta < $max; $delta++) {
956 // Add a new empty item if it doesn't exist yet at this delta.
957 if (!isset($items[$delta])) {
958 $items->appendItem();
961 // For multiple fields, title and description are handled by the wrapping
964 '#title' => $is_multiple ? '' : $field_title,
965 '#description' => $is_multiple ? '' : $description,
967 $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
970 // Input field for the delta (drag-n-drop reordering).
972 // We name the element '_weight' to avoid clashing with elements
973 // defined by widget.
974 $element['_weight'] = array(
976 '#title' => $this->t('Weight for row @number', array('@number' => $delta + 1)),
977 '#title_display' => 'invisible',
978 // Note: this 'delta' is the FAPI #type 'weight' element's property.
980 '#default_value' => $items[$delta]->_weight ?: $delta,
985 // Access for the top element is set to FALSE only when the paragraph
986 // was removed. A paragraphs that a user can not edit has access on
988 if (isset($element['#access']) && !$element['#access']) {
989 $this->realItemCount--;
992 $elements[$delta] = $element;
998 $field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
999 $field_state['real_item_count'] = $this->realItemCount;
1000 $field_state['add_mode'] = $this->getSetting('add_mode');
1001 static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
1004 '#element_validate' => [[$this, 'multipleElementValidate']],
1005 '#required' => $this->fieldDefinition->isRequired(),
1006 '#field_name' => $field_name,
1007 '#cardinality' => $cardinality,
1008 '#max_delta' => $max - 1,
1011 if ($this->realItemCount > 0) {
1013 '#theme' => 'field_multiple_value_form',
1014 '#cardinality_multiple' => $is_multiple,
1015 '#title' => $field_title,
1016 '#description' => $description,
1021 $classes = $this->fieldDefinition->isRequired() ? ['form-required'] : [];
1023 '#type' => 'container',
1024 '#theme_wrappers' => ['container'],
1025 '#cardinality_multiple' => TRUE,
1027 '#type' => 'html_tag',
1029 '#value' => $field_title,
1030 '#attributes' => ['class' => $classes],
1033 '#type' => 'container',
1035 '#markup' => $this->t('No @title added yet.', ['@title' => $this->getSetting('title')]),
1036 '#prefix' => '<em>',
1037 '#suffix' => '</em>',
1043 $elements['description'] = [
1044 '#type' => 'container',
1045 'value' => ['#markup' => $description],
1046 '#attributes' => ['class' => ['description']],
1051 $host = $items->getEntity();
1052 $this->initIsTranslating($form_state, $host);
1054 if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) {
1055 $elements['add_more'] = $this->buildAddActions();
1058 $elements['#attached']['library'][] = 'paragraphs/drupal.paragraphs.widget';
1066 public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
1067 $parents = $form['#parents'];
1069 // Identify the manage field settings default value form.
1070 if (in_array('default_value_input', $parents, TRUE)) {
1071 // Since the entity is not reusable neither cloneable, having a default
1072 // value is not supported.
1073 return ['#markup' => $this->t('No widget available for: %label.', ['%label' => $items->getFieldDefinition()->getLabel()])];
1076 return parent::form($items, $form, $form_state, $get_delta);
1080 * Returns a list of child paragraphs for a given field to loop over.
1082 * @param \Drupal\Core\Form\FormStateInterface $form_state
1084 * @param string $field_name
1085 * The field name for which to find child paragraphs.
1086 * @param \Drupal\paragraphs\ParagraphInterface $paragraph
1087 * The current paragraph.
1088 * @param array $array_parents
1089 * The current field parent structure.
1091 * @return \Drupal\paragraphs\Entity\Paragraph[]
1094 protected function getChildParagraphs(FormStateInterface $form_state, $field_name, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
1096 // Convert the parents structure which only includes field names and delta
1097 // to the full storage array key which includes a prefix and a subform.
1098 $full_parents_key = ['field_storage', '#parents'];
1099 foreach ($array_parents as $i => $parent) {
1100 $full_parents_key[] = $parent;
1102 $full_parents_key[] = 'subform';
1106 $current_parents = array_merge($full_parents_key, ['#fields', $field_name]);
1107 $child_field_state = NestedArray::getValue($form_state->getStorage(), $current_parents);
1109 if ($child_field_state && isset($child_field_state['paragraphs'])) {
1110 // Fetch the paragraphs from the field state. Use the original delta
1111 // to get the right position. Also reorder the paragraphs in the widget
1112 // state accordingly.
1113 $new_widget_paragraphs = [];
1114 foreach ($child_field_state['paragraphs'] as $child_delta => $child_field_item_state) {
1115 $entities[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state['entity'];
1116 $new_widget_paragraphs[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state;
1120 // Set the orderd paragraphs into the widget state and reset original
1122 ksort($new_widget_paragraphs);
1123 $child_field_state['paragraphs'] = $new_widget_paragraphs;
1124 $child_field_state['original_deltas'] = range(0, count($child_field_state['paragraphs']) - 1);
1125 NestedArray::setValue($form_state->getStorage(), $current_parents, $child_field_state);
1127 elseif ($paragraph) {
1128 // If there is no field state, return the paragraphs directly from the
1130 foreach ($paragraph->get($field_name) as $child_delta => $item) {
1131 if ($item->entity) {
1132 $entities[$child_delta] = $item->entity;
1141 * Builds the nested drag and drop structure.
1143 * @param \Drupal\Core\Form\FormStateInterface $form_state
1145 * @param \Drupal\paragraphs\ParagraphInterface|null $paragraph
1146 * The parent paragraph, NULL for the initial call.
1147 * @param string[] $array_parents
1148 * The array parents for nested paragraphs.
1151 * The built form structure.
1153 protected function buildNestedParagraphsFoDragDrop(FormStateInterface $form_state, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
1154 // Look for nested elements.
1156 $field_definitions = [];
1158 foreach ($paragraph->getFieldDefinitions() as $child_field_name => $field_definition) {
1159 /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
1160 if ($field_definition->getType() == 'entity_reference_revisions' && $field_definition->getSetting('target_type') == 'paragraph') {
1161 $field_definitions[$child_field_name] = $field_definition;
1166 $field_definitions = [$this->fieldDefinition->getName() => $this->fieldDefinition];
1169 /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
1170 foreach ($field_definitions as $child_field_name => $field_definition) {
1171 $child_path = implode('][', array_merge($array_parents, [$child_field_name]));
1172 $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality();
1173 $allowed_types = implode(array_keys($this->getAllowedTypes($field_definition)), ',');
1174 $elements[$child_field_name] = [
1175 '#type' => 'container',
1176 '#attributes' => ['class' => ['paragraphs-dragdrop-wrapper']],
1179 // Only show a field label if there is more than one paragraph field.
1180 $label = count($field_definitions) > 1 || !$paragraph ? '<label><strong>' . $field_definition->getLabel() . '</strong></label>' : '';
1182 $elements[$child_field_name]['list'] = [
1183 '#type' => 'markup',
1184 '#prefix' => $label . '<ul class="paragraphs-dragdrop" data-paragraphs-dragdrop-cardinality="' . $cardinality . '" data-paragraphs-dragdrop-allowed-types="' . $allowed_types . '" data-paragraphs-dragdrop-path="' . $child_path . '">',
1185 '#suffix' => '</ul>',
1188 /** @var \Drupal\paragraphs\Entity\Paragraph $child_paragraph */
1189 foreach ($this->getChildParagraphs($form_state, $child_field_name, $paragraph, $array_parents) as $child_delta => $child_paragraph) {
1192 '#type' => 'container',
1193 '#attributes' => ['class' => ['paragraphs-summary-wrapper']],
1195 $element['top']['paragraph_summary']['type'] = [
1196 '#markup' => '<strong>' . $child_paragraph->getParagraphType()->label() . '</strong>',
1199 // We name the element '_weight' to avoid clashing with elements
1200 // defined by widget.
1201 $element['_weight'] = array(
1202 '#type' => 'hidden',
1203 '#default_value' => $child_delta,
1205 'class' => ['paragraphs-dragdrop__weight'],
1209 $element['_path'] = [
1210 '#type' => 'hidden',
1211 '#title' => $this->t('Current path for @number', ['@number' => $delta = 1]),
1212 '#title_display' => 'invisible',
1213 '#default_value' => $child_path,
1215 'class' => ['paragraphs-dragdrop__path'],
1219 $summary_options = [];
1221 $element['#prefix'] = '<li data-paragraphs-dragdrop-bundle="' . $child_paragraph->bundle() . '"><a href="#" class="tabledrag-handle"><div class="handle"> </div></a>';
1222 $element['#suffix'] = '</li>';
1223 $child_array_parents = array_merge($array_parents, [$child_field_name, $child_delta]);
1225 if ($child_elements = $this->buildNestedParagraphsFoDragDrop($form_state, $child_paragraph, $child_array_parents)) {
1226 $element['dragdrop'] = $child_elements;
1228 // Set the depth limit to 0 to avoid displaying a summary for the
1230 $summary_options['depth_limit'] = 0;
1233 $element['top']['summary']['fields_info'] = [
1234 '#markup' => $child_paragraph->getSummary($summary_options),
1235 '#prefix' => '<div class="paragraphs-collapsed-description">',
1236 '#suffix' => '</div>',
1239 $elements[$child_field_name]['list'][$child_delta] = $element;
1246 * Add 'add more' button, if not working with a programmed form.
1249 * The form element array.
1251 protected function buildAddActions() {
1252 if (count($this->getAccessibleOptions()) === 0) {
1253 if (count($this->getAllowedTypes()) === 0) {
1254 $add_more_elements['info'] = $this->createMessage($this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]));
1257 $add_more_elements['info'] = $this->createMessage($this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]));
1260 return $add_more_elements;
1263 if (in_array($this->getSetting('add_mode'), ['button', 'dropdown', 'modal'])) {
1264 return $this->buildButtonsAddMode();
1267 return $this->buildSelectAddMode();
1271 * Returns the available paragraphs type.
1274 * Available paragraphs types.
1276 protected function getAccessibleOptions() {
1277 if ($this->accessOptions !== NULL) {
1278 return $this->accessOptions;
1281 $entity_type_manager = \Drupal::entityTypeManager();
1282 $target_type = $this->getFieldSetting('target_type');
1283 $bundles = $this->getAllowedTypes();
1284 $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
1285 $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
1287 foreach ($bundles as $machine_name => $bundle) {
1288 if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
1289 || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
1290 if ($access_control_handler->createAccess($machine_name)) {
1291 $this->accessOptions[$machine_name] = $bundle['label'];
1296 return $this->accessOptions;
1300 * Helper to create a paragraph UI message.
1302 * @param string $message
1304 * @param string $type
1308 * Render array of message.
1310 public function createMessage($message, $type = 'warning') {
1312 '#type' => 'container',
1313 '#markup' => $message,
1314 '#attributes' => ['class' => ['messages', 'messages--' . $type]],
1319 * Expand button base array into a paragraph widget action button.
1321 * @param array $button_base
1322 * Button base render array.
1325 * Button render array.
1327 public static function expandButton(array $button_base) {
1328 // Do not expand elements that do not have submit handler.
1329 if (empty($button_base['#submit'])) {
1330 return $button_base;
1333 $button = $button_base + [
1334 '#type' => 'submit',
1335 '#theme_wrappers' => ['input__submit__paragraph_action'],
1338 // Html::getId will give us '-' char in name but we want '_' for now so
1339 // we use strtr to search&replace '-' to '_'.
1340 $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_');
1341 $button['#id'] = Html::getUniqueId($button['#name']);
1343 if (isset($button['#ajax'])) {
1344 $button['#ajax'] += [
1346 // Since a normal throbber is added inline, this has the potential to
1347 // break a layout if the button is located in dropbuttons. Instead,
1348 // it's safer to just show the fullscreen progress element instead.
1349 'progress' => ['type' => 'fullscreen'],
1357 * Get common submit element information for processing ajax submit handlers.
1359 * @param array $form
1361 * @param FormStateInterface $form_state
1362 * Form state object.
1363 * @param int $position
1364 * Position of triggering element.
1367 * Submit element information.
1369 public static function getSubmitElementInfo(array $form, FormStateInterface $form_state, $position = ParagraphsWidget::ACTION_POSITION_BASE) {
1370 $submit['button'] = $form_state->getTriggeringElement();
1372 // Go up in the form, to the widgets container.
1373 if ($position == ParagraphsWidget::ACTION_POSITION_BASE) {
1374 $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -2));
1376 if ($position == ParagraphsWidget::ACTION_POSITION_HEADER) {
1377 $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -3));
1379 elseif ($position == ParagraphsWidget::ACTION_POSITION_ACTIONS) {
1380 $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -5));
1381 $delta = array_slice($submit['button']['#array_parents'], -5, -4);
1382 $submit['delta'] = $delta[0];
1385 $submit['field_name'] = $submit['element']['#field_name'];
1386 $submit['parents'] = $submit['element']['#field_parents'];
1388 // Get widget state.
1389 $submit['widget_state'] = static::getWidgetState($submit['parents'], $submit['field_name'], $form_state);
1395 * Build drop button.
1397 * @param array $elements
1398 * Elements for drop button.
1401 * Drop button array.
1403 protected function buildDropbutton(array $elements = []) {
1405 '#type' => 'container',
1406 '#attributes' => ['class' => ['paragraphs-dropbutton-wrapper']],
1410 // Because we are cloning the elements into title sub element we need to
1411 // sort children first.
1412 foreach (Element::children($elements, TRUE) as $child) {
1413 // Clone the element as an operation.
1414 $operations[$child] = ['title' => $elements[$child]];
1416 // Flag the original element as printed so it doesn't render twice.
1417 $elements[$child]['#printed'] = TRUE;
1420 $build['operations'] = [
1421 '#type' => 'paragraph_operations',
1422 // Even though operations are run through the "links" element type, the
1423 // theme system will render any render array passed as a link "title".
1424 '#links' => $operations,
1427 return $build + $elements;
1431 * Builds dropdown button for adding new paragraph.
1434 * The form element array.
1436 protected function buildButtonsAddMode() {
1437 $options = $this->getAccessibleOptions();
1438 $add_mode = $this->getSetting('add_mode');
1439 $paragraphs_type_storage = \Drupal::entityTypeManager()->getStorage('paragraphs_type');
1441 // Build the buttons.
1442 $add_more_elements = [];
1443 foreach ($options as $machine_name => $label) {
1444 $button_key = 'add_more_button_' . $machine_name;
1445 $add_more_elements[$button_key] = $this->expandButton([
1446 '#type' => 'submit',
1447 '#name' => $this->fieldIdPrefix . '_' . $machine_name . '_add_more',
1448 '#value' => $add_mode == 'modal' ? $label : $this->t('Add @type', ['@type' => $label]),
1449 '#attributes' => ['class' => ['field-add-more-submit']],
1450 '#limit_validation_errors' => [array_merge($this->fieldParents, [$this->fieldDefinition->getName(), 'add_more'])],
1451 '#submit' => [[get_class($this), 'addMoreSubmit']],
1453 'callback' => [get_class($this), 'addMoreAjax'],
1454 'wrapper' => $this->fieldWrapperId,
1456 '#bundle_machine_name' => $machine_name,
1459 if ($add_mode === 'modal' && $icon_url = $paragraphs_type_storage->load($machine_name)->getIconUrl()) {
1460 $add_more_elements[$button_key]['#attributes']['style'] = 'background-image: url(' . $icon_url . ');';
1464 // Determine if buttons should be rendered as dropbuttons.
1465 if (count($options) > 1 && $add_mode == 'dropdown') {
1466 $add_more_elements = $this->buildDropbutton($add_more_elements);
1467 $add_more_elements['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
1469 elseif ($add_mode == 'modal') {
1470 $this->buildModalAddForm($add_more_elements);
1471 $add_more_elements['add_modal_form_area']['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
1473 $add_more_elements['#weight'] = 1;
1475 return $add_more_elements;
1479 * Builds list of actions based on paragraphs type.
1482 * The form element array.
1484 protected function buildSelectAddMode() {
1485 $field_name = $this->fieldDefinition->getName();
1486 $field_title = $this->fieldDefinition->getLabel();
1487 $setting_title = $this->getSetting('title');
1488 $add_more_elements['add_more_select'] = [
1489 '#type' => 'select',
1490 '#options' => $this->getAccessibleOptions(),
1491 '#title' => $this->t('@title type', ['@title' => $setting_title]),
1492 '#label_display' => 'hidden',
1495 $text = $this->t('Add @title', ['@title' => $setting_title]);
1497 if ($this->realItemCount > 0) {
1498 $text = $this->t('Add another @title', ['@title' => $setting_title]);
1501 $add_more_elements['add_more_button'] = [
1502 '#type' => 'submit',
1503 '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more',
1505 '#attributes' => ['class' => ['field-add-more-submit']],
1506 '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
1507 '#submit' => [[get_class($this), 'addMoreSubmit']],
1509 'callback' => [get_class($this), 'addMoreAjax'],
1510 'wrapper' => $this->fieldWrapperId,
1515 $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $field_title]);
1516 return $add_more_elements;
1522 public static function addMoreAjax(array $form, FormStateInterface $form_state) {
1523 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
1524 $element = $submit['element'];
1526 // Add a DIV around the delta receiving the Ajax effect.
1527 $delta = $submit['element']['#max_delta'];
1528 $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
1529 $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
1535 * Ajax callback for all actions.
1537 public static function allActionsAjax(array $form, FormStateInterface $form_state) {
1538 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
1539 $element = $submit['element'];
1541 // Add a DIV around the delta receiving the Ajax effect.
1542 $delta = $submit['element']['#max_delta'];
1543 $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
1544 $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
1552 public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
1553 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
1555 if ($submit['widget_state']['real_item_count'] < $submit['element']['#cardinality'] || $submit['element']['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
1556 $submit['widget_state']['items_count']++;
1559 if (isset($submit['button']['#bundle_machine_name'])) {
1560 $submit['widget_state']['selected_bundle'] = $submit['button']['#bundle_machine_name'];
1563 $submit['widget_state']['selected_bundle'] = $submit['element']['add_more']['add_more_select']['#value'];
1566 $submit['widget_state'] = static::autocollapse($submit['widget_state']);
1568 static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
1570 $form_state->setRebuild();
1574 * Creates a duplicate of the paragraph entity.
1576 public static function duplicateSubmit(array $form, FormStateInterface $form_state) {
1577 $button = $form_state->getTriggeringElement();
1578 // Go one level up in the form, to the widgets container.
1579 $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -5));
1580 $field_name = $element['#field_name'];
1581 $parents = $element['#field_parents'];
1583 // Inserting new element in the array.
1584 $widget_state = static::getWidgetState($parents, $field_name, $form_state);
1586 // Map the button delta to the actual delta.
1587 $original_button_delta = $button['#delta'];
1588 $current_button_delta = array_search($button['#delta'], $widget_state['original_deltas']);
1590 $widget_state['items_count']++;
1591 $widget_state['real_item_count']++;
1593 // Initialize the new original delta map with the new entry.
1594 $new_original_deltas = [
1595 $current_button_delta + 1 => count($widget_state['original_deltas']),
1598 $user_input = NestedArray::getValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5));
1599 $user_input[count($widget_state['original_deltas'])]['_weight'] = $current_button_delta + 1;
1601 // Increase all original deltas bigger than the delta of the duplicated
1603 foreach ($widget_state['original_deltas'] as $current_delta => $original_delta) {
1604 $new_delta = $current_delta > $current_button_delta ? $current_delta + 1 : $current_delta;
1605 $new_original_deltas[$new_delta] = $original_delta;
1606 $user_input[$original_delta]['_weight'] = $new_delta;
1608 $widget_state['original_deltas'] = $new_original_deltas;
1609 /** @var \Drupal\Core\Entity\EntityInterface $entity */
1610 $entity = $widget_state['paragraphs'][$original_button_delta]['entity'];
1612 $widget_state = static::autocollapse($widget_state);
1614 // Check if the replicate module is enabled.
1615 if (\Drupal::hasService('replicate.replicator')) {
1616 $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($entity);
1619 $duplicate_entity = $entity->createDuplicate();
1621 // Create the duplicated paragraph and insert it below the original.
1622 $widget_state['paragraphs'][] = [
1623 'entity' => $duplicate_entity,
1624 'display' => $widget_state['paragraphs'][$original_button_delta]['display'],
1628 NestedArray::setValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5), $user_input);
1629 static::setWidgetState($parents, $field_name, $form_state, $widget_state);
1630 $form_state->setRebuild();
1633 public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
1634 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
1636 $new_mode = $submit['button']['#paragraphs_mode'];
1638 if ($new_mode === 'edit') {
1639 $submit['widget_state'] = static::autocollapse($submit['widget_state']);
1642 $submit['widget_state']['paragraphs'][$submit['delta']]['mode'] = $new_mode;
1644 if (!empty($submit['button']['#paragraphs_show_warning'])) {
1645 $submit['widget_state']['paragraphs'][$submit['delta']]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
1648 static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
1650 $form_state->setRebuild();
1653 public static function itemAjax(array $form, FormStateInterface $form_state) {
1654 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
1656 $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
1657 $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
1659 return $submit['element'];
1663 * Sets the form mode accordingly.
1665 * @param array $form
1666 * An associate array containing the structure of the form.
1667 * @param \Drupal\Core\Form\FormStateInterface $form_state
1668 * The current state of the form.
1670 public static function dragDropModeSubmit(array $form, FormStateInterface $form_state) {
1671 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
1673 if (empty($submit['widget_state']['dragdrop'])) {
1674 $submit['widget_state']['dragdrop'] = TRUE;
1677 $submit['widget_state']['dragdrop'] = FALSE;
1680 // Make sure that flag that we already reordered is unset when the mode is
1682 unset($submit['widget_state']['reordered']);
1684 // Switch the form mode accordingly.
1685 static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
1687 $form_state->setRebuild();
1692 * Reorder paragraphs.
1694 * @param \Drupal\Core\Form\FormStateInterface $form_state
1696 * @param $field_values_parents
1697 * The field value parents.
1699 protected static function reorderParagraphs(FormStateInterface $form_state, $field_values_parents) {
1700 $field_name = end($field_values_parents);
1701 $field_values = NestedArray::getValue($form_state->getValues(), $field_values_parents);
1702 $complete_field_storage = NestedArray::getValue(
1703 $form_state->getStorage(), [
1708 $new_field_storage = $complete_field_storage;
1710 // Set a flag to prevent this from running twice, as the entity is built
1711 // for validation as well as saving and would fail the second time as we
1712 // already altered the field storage.
1713 if (!empty($new_field_storage['#fields'][$field_name]['reordered'])) {
1716 $new_field_storage['#fields'][$field_name]['reordered'] = TRUE;
1718 // Clear out all current paragraphs keys in all nested paragraph widgets
1719 // as there might be fewer than before or none in a certain widget.
1720 $clear_paragraphs = function ($field_storage) use (&$clear_paragraphs) {
1721 foreach ($field_storage as $key => $value) {
1722 if ($key === '#fields') {
1723 foreach ($value as $field_name => $widget_state) {
1724 if (isset($widget_state['paragraphs'])) {
1725 $field_storage['#fields'][$field_name]['paragraphs'] = [];
1730 $field_storage[$key] = $clear_paragraphs($field_storage[$key]);
1733 return $field_storage;
1736 // Only clear the current field and its children to avoid deleting
1737 // paragraph references in other fields.
1738 $new_field_storage['#fields'][$field_name]['paragraphs'] = [];
1739 if (isset($new_field_storage[$field_name])) {
1740 $new_field_storage[$field_name] = $clear_paragraphs($new_field_storage[$field_name]);
1743 $reorder_paragraphs = function ($reorder_values, $parents = [], FieldableEntityInterface $parent_entity = NULL) use ($complete_field_storage, &$new_field_storage, &$reorder_paragraphs) {
1744 foreach ($reorder_values as $field_name => $values) {
1745 foreach ($values['list'] as $delta => $item_values) {
1746 $old_keys = array_merge(
1754 $path = explode('][', $item_values['_path']);
1755 $new_field_name = array_pop($path);
1757 foreach ($path as $i => $key) {
1758 $key_parents[] = $key;
1760 $key_parents[] = 'subform';
1763 $new_keys = array_merge(
1768 $item_values['_weight']
1772 $item_state = NestedArray::getValue($complete_field_storage, $old_keys, $key_exists);
1773 if (!$key_exists && $parent_entity) {
1774 // If key does not exist, then this parent widget was previously
1775 // not expanded. This can only happen on nested levels. In that
1776 // case, initialize a new item state and set the widget state to
1777 // an empty array if it is not already set from an earlier item.
1778 // If something else is placed there, it will be put in there,
1779 // otherwise the widget will know that nothing is there anymore.
1781 'entity' => $parent_entity->get($field_name)->get($delta)->entity,
1784 $widget_state_keys = array_slice($old_keys, 0, count($old_keys) - 2);
1785 if (!NestedArray::getValue($new_field_storage, $widget_state_keys)) {
1786 NestedArray::setValue($new_field_storage, $widget_state_keys, ['paragraphs' => []]);
1790 // Ensure the referenced paragraph will be saved.
1791 $item_state['entity']->setNeedsSave(TRUE);
1793 NestedArray::setValue($new_field_storage, $new_keys, $item_state);
1794 if (isset($item_values['dragdrop'])) {
1795 $reorder_paragraphs(
1796 $item_values['dragdrop'], array_merge(
1802 ), $item_state['entity']
1808 $reorder_paragraphs($field_values['dragdrop']);
1810 // Recalculate original deltas.
1811 $recalculate_original_deltas = function ($field_storage, ContentEntityInterface $parent_entity) use (&$recalculate_original_deltas) {
1812 if (isset($field_storage['#fields'])) {
1813 foreach ($field_storage['#fields'] as $field_name => $widget_state) {
1814 if (isset($widget_state['paragraphs'])) {
1816 // If the parent field does not exist but we have paragraphs in
1817 // widget state, something went wrong and we have a mismatch.
1818 // Throw an exception.
1819 if (!$parent_entity->hasField($field_name) && !empty($widget_state['paragraphs'])) {
1820 throw new \LogicException('Reordering paragraphs resulted in paragraphs on non-existing field ' . $field_name . ' on parent entity ' . $parent_entity->getEntityTypeId() . '/' . $parent_entity->id());
1823 // Sort the paragraphs by key so that they will be assigned to
1824 // the entity in the right order. Reset the deltas.
1825 ksort($widget_state['paragraphs']);
1826 $widget_state['paragraphs'] = array_values($widget_state['paragraphs']);
1828 $original_deltas = range(0, count($widget_state['paragraphs']) - 1);
1829 $field_storage['#fields'][$field_name]['original_deltas'] = $original_deltas;
1830 $field_storage['#fields'][$field_name]['items_count'] = count($widget_state['paragraphs']);
1831 $field_storage['#fields'][$field_name]['real_item_count'] = count($widget_state['paragraphs']);
1833 // Update the parent entity and point to the new children, if the
1834 // parent field does not exist, we also have no paragraphs, so
1835 // we can just skip this, this is a dead leaf after re-ordering.
1836 // @todo Clean this up somehow?
1837 if ($parent_entity->hasField($field_name)) {
1838 $parent_entity->set($field_name, array_column($widget_state['paragraphs'], 'entity'));
1840 // Next process that field recursively.
1841 foreach (array_keys($widget_state['paragraphs']) as $delta) {
1842 if (isset($field_storage[$field_name][$delta]['subform'])) {
1843 $field_storage[$field_name][$delta]['subform'] = $recalculate_original_deltas($field_storage[$field_name][$delta]['subform'], $parent_entity->get($field_name)->get($delta)->entity);
1851 return $field_storage;
1854 $parent_entity = $form_state->getFormObject()->getEntity();
1855 $new_field_storage = $recalculate_original_deltas($new_field_storage, $parent_entity);
1857 $form_state->set(['field_storage', '#parents'], $new_field_storage);
1861 * Ajax callback for the dragdrop mode.
1863 * @param array $form
1864 * An associate array containing the structure of the form.
1865 * @param \Drupal\Core\Form\FormStateInterface $form_state
1866 * The current state of the form.
1869 * The container form element.
1871 public static function dragDropModeAjax(array $form, FormStateInterface $form_state) {
1872 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
1874 $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
1875 $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
1877 return $submit['element'];
1881 * Returns the value of a setting for the entity reference selection handler.
1883 * @param string $setting_name
1887 * The setting value.
1889 protected function getSelectionHandlerSetting($setting_name) {
1890 $settings = $this->getFieldSetting('handler_settings');
1891 return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
1897 public function elementValidate($element, FormStateInterface $form_state, $form) {
1898 $field_name = $this->fieldDefinition->getName();
1899 $widget_state = static::getWidgetState($element['#field_parents'], $field_name, $form_state);
1900 $delta = $element['#delta'];
1902 if (isset($widget_state['paragraphs'][$delta]['entity'])) {
1903 /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
1904 $entity = $widget_state['paragraphs'][$delta]['entity'];
1906 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
1907 $display = $widget_state['paragraphs'][$delta]['display'];
1909 if ($widget_state['paragraphs'][$delta]['mode'] == 'edit') {
1910 // Extract the form values on submit for getting the current paragraph.
1911 $display->extractFormValues($entity, $element['subform'], $form_state);
1912 $display->validateFormValues($entity, $element['subform'], $form_state);
1914 // Validate all enabled behavior plugins.
1915 $paragraphs_type = $entity->getParagraphType();
1916 if (\Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
1917 foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
1918 $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
1919 $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state);
1925 static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state);
1931 public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
1932 $field_name = $this->fieldDefinition->getName();
1934 $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1936 // In dragdrop mode, validation errors can not be mapped to form elements,
1937 // add them on the top level widget element.
1938 if (!empty($field_state['dragdrop'])) {
1939 if ($violations->count()) {
1940 $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']);
1941 foreach ($violations as $violation) {
1942 $form_state->setError($element, $violation->getMessage());
1947 return parent::flagErrors($items, $violations, $form, $form_state);
1952 * Special handling to validate form elements with multiple values.
1954 * @param array $elements
1955 * An associative array containing the substructure of the form to be
1956 * validated in this call.
1957 * @param \Drupal\Core\Form\FormStateInterface $form_state
1958 * The current state of the form.
1959 * @param array $form
1960 * The complete form array.
1962 public function multipleElementValidate(array $elements, FormStateInterface $form_state, array $form) {
1963 $field_name = $this->fieldDefinition->getName();
1964 $widget_state = static::getWidgetState($elements['#field_parents'], $field_name, $form_state);
1966 if ($elements['#required'] && $widget_state['real_item_count'] < 1) {
1967 $form_state->setError($elements, t('@name field is required.', ['@name' => $this->fieldDefinition->getLabel()]));
1970 static::setWidgetState($elements['#field_parents'], $field_name, $form_state, $widget_state);
1976 public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
1977 $field_name = $this->fieldDefinition->getName();
1978 $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1979 $element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']);
1981 if (!empty($widget_state['dragdrop'])) {
1982 $path = array_merge($form['#parents'], array($field_name));
1983 static::reorderParagraphs($form_state, $path);
1985 // After re-ordering, get the updated widget state.
1986 $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
1988 // Re-create values based on current widget state.
1990 foreach ($widget_state['paragraphs'] as $delta => $paragraph_state) {
1991 $values[$delta]['entity'] = $paragraph_state['entity'];
1996 foreach ($values as $delta => &$item) {
1997 if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
1998 && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
1999 /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
2000 $paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity'];
2002 /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
2003 $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
2004 if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') {
2005 $display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state);
2007 // A content entity form saves without any rebuild. It needs to set the
2008 // language to update it in case of language change.
2009 $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
2010 if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) {
2011 // If a translation in the given language already exists, switch to
2012 // that. If there is none yet, update the language.
2013 if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) {
2014 $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode'));
2017 $paragraphs_entity->set($langcode_key, $form_state->get('langcode'));
2020 if (isset($item['behavior_plugins'])) {
2021 // Submit all enabled behavior plugins.
2022 $paragraphs_type = $paragraphs_entity->getParagraphType();
2023 foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
2024 if (!isset($item['behavior_plugins'][$plugin_id])) {
2025 $item['behavior_plugins'][$plugin_id] = [];
2027 $original_delta = $item['_original_delta'];
2028 if (isset($element[$original_delta]) && isset($element[$original_delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm() && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
2029 $subform_state = SubformState::createForSubform($element[$original_delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
2030 if (isset($item['behavior_plugins'][$plugin_id])) {
2031 $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state);
2037 $paragraphs_entity->setNeedsSave(TRUE);
2038 $item['entity'] = $paragraphs_entity;
2039 $item['target_id'] = $paragraphs_entity->id();
2040 $item['target_revision_id'] = $paragraphs_entity->getRevisionId();
2042 // If our mode is remove don't save or reference this entity.
2043 // @todo: Maybe we should actually delete it here?
2044 elseif (isset($widget_state['paragraphs'][$item['_original_delta']]['mode']) && $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') {
2045 $item['target_id'] = NULL;
2046 $item['target_revision_id'] = NULL;
2055 public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
2056 // Filter possible empty items.
2057 $items->filterEmptyItems();
2059 // Remove buttons from header actions.
2060 $field_name = $this->fieldDefinition->getName();
2061 $path = array_merge($form['#parents'], array($field_name));
2062 $form_state_variables = $form_state->getValues();
2064 $values = NestedArray::getValue($form_state_variables, $path, $key_exists);
2067 unset($values['header_actions']);
2069 NestedArray::setValue($form_state_variables, $path, $values);
2070 $form_state->setValues($form_state_variables);
2073 return parent::extractFormValues($items, $form, $form_state);
2077 * Initializes the translation form state.
2079 * @param \Drupal\Core\Form\FormStateInterface $form_state
2080 * @param \Drupal\Core\Entity\ContentEntityInterface $host
2082 protected function initIsTranslating(FormStateInterface $form_state, ContentEntityInterface $host) {
2083 if ($this->isTranslating != NULL) {
2086 $this->isTranslating = FALSE;
2087 if (!$host->isTranslatable()) {
2090 if (!$host->getEntityType()->hasKey('default_langcode')) {
2093 $default_langcode_key = $host->getEntityType()->getKey('default_langcode');
2094 if (!$host->hasField($default_langcode_key)) {
2098 if (!empty($form_state->get('content_translation'))) {
2099 // Adding a language through the ContentTranslationController.
2100 $this->isTranslating = TRUE;
2102 if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) {
2103 // Editing a translation.
2104 $this->isTranslating = TRUE;
2109 * After-build callback for removing the translatability clue from the widget.
2111 * If the fields on the paragraph type are translatable,
2112 * ContentTranslationHandler::addTranslatabilityClue()adds an
2113 * "(all languages)" suffix to the widget title. That suffix is incorrect and
2114 * is being removed by this method using a #after_build on the field widget.
2116 * @param array $element
2117 * @param \Drupal\Core\Form\FormStateInterface $form_state
2121 public static function removeTranslatabilityClue(array $element, FormStateInterface $form_state) {
2122 // Widgets could have multiple elements with their own titles, so remove the
2123 // suffix if it exists, do not recurse lower than this to avoid going into
2124 // nested paragraphs or similar nested field types.
2125 $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>';
2126 if (isset($element['#title']) && strpos($element['#title'], $suffix)) {
2127 $element['#title'] = str_replace($suffix, '', $element['#title']);
2129 // Loop over all widget deltas.
2130 foreach (Element::children($element) as $delta) {
2131 if (isset($element[$delta]['#title']) && strpos($element[$delta]['#title'], $suffix)) {
2132 $element[$delta]['#title'] = str_replace($suffix, '', $element[$delta]['#title']);
2134 // Loop over all form elements within the current delta.
2135 foreach (Element::children($element[$delta]) as $field) {
2136 if (isset($element[$delta][$field]['#title']) && strpos($element[$delta][$field]['#title'], $suffix)) {
2137 $element[$delta][$field]['#title'] = str_replace($suffix, '', $element[$delta][$field]['#title']);
2145 * Returns the default paragraph type.
2148 * Label name for default paragraph type.
2150 protected function getDefaultParagraphTypeLabelName() {
2151 if ($this->getDefaultParagraphTypeMachineName() !== NULL) {
2152 $allowed_types = $this->getAllowedTypes();
2153 return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label'];
2160 * Returns the machine name for default paragraph type.
2163 * Machine name for default paragraph type.
2165 protected function getDefaultParagraphTypeMachineName() {
2166 $default_type = $this->getSetting('default_paragraph_type');
2167 $allowed_types = $this->getAllowedTypes();
2168 if ($default_type && isset($allowed_types[$default_type])) {
2169 return $default_type;
2171 // Check if the user explicitly selected not to have any default Paragraph
2172 // type. Othewise, if there is only one type available, that one is the
2174 if ($default_type === '_none') {
2177 if (count($allowed_types) === 1) {
2178 return key($allowed_types);
2185 * Counts the number of paragraphs in a certain mode in a form substructure.
2187 * @param array $widget_state
2188 * The widget state for the form substructure containing information about
2189 * the paragraphs within.
2190 * @param string $mode
2191 * The mode to look for.
2194 * The number of paragraphs is the given mode.
2196 protected function getNumberOfParagraphsInMode(array $widget_state, $mode) {
2197 if (!isset($widget_state['paragraphs'])) {
2201 $paragraphs_count = 0;
2202 foreach ($widget_state['paragraphs'] as $paragraph) {
2203 if ($paragraph['mode'] == $mode) {
2204 $paragraphs_count++;
2208 return $paragraphs_count;
2214 public static function isApplicable(FieldDefinitionInterface $field_definition) {
2215 $target_type = $field_definition->getSetting('target_type');
2216 $paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type);
2217 if ($paragraph_type) {
2218 return $paragraph_type->entityClassImplements(ParagraphInterface::class);
2225 * Builds header actions.
2227 * @param array[] $field_state
2228 * Field widget state.
2229 * @param \Drupal\Core\Form\FormStateInterface $form_state
2230 * Current form state.
2233 * The form element array.
2235 public function buildHeaderActions(array $field_state, FormStateInterface $form_state) {
2237 if (empty($this->fieldParents)) {
2240 '#type' => 'paragraphs_actions',
2243 $field_name = $this->fieldDefinition->getName();
2244 $id_prefix = implode('-', array_merge($this->fieldParents, [$field_name]));
2246 // Only show the dragdrop mode if we can find the sortable library.
2247 $library_discovery = \Drupal::service('library.discovery');
2248 $library = $library_discovery->getLibraryByName('paragraphs', 'paragraphs-dragdrop');
2249 if ($library || \Drupal::state()->get('paragraphs_test_dragdrop_force_show', FALSE)) {
2250 $dragdrop_mode = $this->expandButton([
2251 '#type' => 'submit',
2252 '#name' => $this->fieldIdPrefix . '_dragdrop_mode',
2253 '#value' => !empty($field_state['dragdrop']) ? $this->t('Complete drag & drop') : $this->t('Drag & drop'),
2254 '#attributes' => ['class' => ['field-dragdrop-mode-submit']],
2255 '#submit' => [[get_class($this), 'dragDropModeSubmit']],
2258 'callback' => [get_class($this), 'dragDropModeAjax'],
2259 'wrapper' => $this->fieldWrapperId,
2263 // Make the complete button a primary button, limit validation errors
2264 // only for enabling drag and drop mode.
2265 if (!empty($field_state['dragdrop'])) {
2266 $dragdrop_mode['#button_type'] = 'primary';
2267 $actions['actions']['dragdrop_mode'] = $dragdrop_mode;
2270 $dragdrop_mode['#limit_validation_errors'] = [
2271 array_merge($this->fieldParents, [$field_name, 'dragdrop_mode']),
2273 $actions['dropdown_actions']['dragdrop_mode'] = $dragdrop_mode;
2277 if ($this->realItemCount > 1 && empty($field_state['dragdrop'])) {
2279 $collapse_all = $this->expandButton([
2280 '#type' => 'submit',
2281 '#value' => $this->t('Collapse all'),
2282 '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
2283 '#name' => $id_prefix . '_collapse_all',
2284 '#paragraphs_mode' => 'closed',
2285 '#limit_validation_errors' => [
2286 array_merge($this->fieldParents, [$field_name, 'collapse_all']),
2289 'callback' => [get_class($this), 'allActionsAjax'],
2290 'wrapper' => $this->fieldWrapperId,
2293 '#paragraphs_show_warning' => TRUE,
2296 $edit_all = $this->expandButton([
2297 '#type' => 'submit',
2298 '#value' => $this->t('Edit all'),
2299 '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
2300 '#name' => $id_prefix . '_edit-all',
2301 '#paragraphs_mode' => 'edit',
2302 '#limit_validation_errors' => [],
2304 'callback' => [get_class($this), 'allActionsAjax'],
2305 'wrapper' => $this->fieldWrapperId,
2309 if (isset($field_state['paragraphs'][0]['mode']) && $field_state['paragraphs'][0]['mode'] === 'closed') {
2310 $edit_all['#attributes'] = [
2311 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
2312 'title' => $this->t('Edit all'),
2314 $edit_all['#title'] = $this->t('Edit All');
2315 $actions['actions']['edit_all'] = $edit_all;
2316 $actions['dropdown_actions']['collapse_all'] = $collapse_all;
2319 $collapse_all['#attributes'] = [
2320 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
2321 'title' => $this->t('Collapse all'),
2323 $actions['actions']['collapse_all'] = $collapse_all;
2324 $actions['dropdown_actions']['edit_all'] = $edit_all;
2329 // Add paragraphs_header flag which we use later in preprocessor to move
2330 // header actions to table header.
2332 $actions['#paragraphs_header'] = TRUE;
2339 * Loops through all paragraphs and change mode for each paragraph instance.
2341 * @param array $form
2342 * Current form state.
2343 * @param \Drupal\Core\Form\FormStateInterface $form_state
2344 * Current form state.
2346 public static function changeAllEditModeSubmit(array $form, FormStateInterface $form_state) {
2347 $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
2349 // Change edit mode for each paragraph.
2350 foreach ($submit['widget_state']['paragraphs'] as $delta => &$paragraph) {
2351 if ($submit['widget_state']['paragraphs'][$delta]['mode'] !== 'remove') {
2352 $submit['widget_state']['paragraphs'][$delta]['mode'] = $submit['button']['#paragraphs_mode'];
2353 if (!empty($submit['button']['#paragraphs_show_warning'])) {
2354 $submit['widget_state']['paragraphs'][$delta]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
2359 // Disable autocollapse when editing all and enable it when closing all.
2360 if ($submit['button']['#paragraphs_mode'] === 'edit') {
2361 $submit['widget_state']['autocollapse'] = 'none';
2363 elseif ($submit['button']['#paragraphs_mode'] === 'closed') {
2364 $submit['widget_state']['autocollapse'] = 'all';
2367 static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
2368 $form_state->setRebuild();
2372 * Returns a state with all paragraphs closed, if autocollapse is enabled.
2374 * @param array $widget_state
2375 * The current widget state.
2378 * The widget state altered by closing all paragraphs.
2380 public static function autocollapse(array $widget_state) {
2381 if ($widget_state['real_item_count'] > 0 && $widget_state['autocollapse'] !== 'none') {
2382 foreach ($widget_state['paragraphs'] as $delta => $value) {
2383 if ($widget_state['paragraphs'][$delta]['mode'] === 'edit') {
2384 $widget_state['paragraphs'][$delta]['mode'] = 'closed';
2389 return $widget_state;