use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Render\Element;
-use Drupal\paragraphs;
use Drupal\paragraphs\ParagraphInterface;
+use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Plugin implementation of the 'entity_reference_revisions paragraphs' widget.
*/
class ParagraphsWidget extends WidgetBase {
+ /**
+ * Action position is in the add paragraphs place.
+ */
+ const ACTION_POSITION_BASE = 1;
+
+ /**
+ * Action position is in the table header section.
+ */
+ const ACTION_POSITION_HEADER = 2;
+
+ /**
+ * Action position is in the actions section of the widget.
+ */
+ const ACTION_POSITION_ACTIONS = 3;
+
/**
* Indicates whether the current widget instance is in translation.
*
*/
protected $accessOptions = NULL;
+ /**
+ * Constructs a ParagraphsWidget object.
+ *
+ * @param string $plugin_id
+ * The plugin_id for the widget.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+ * The definition of the field to which the widget is associated.
+ * @param array $settings
+ * The widget settings.
+ * @param array $third_party_settings
+ * Any third party settings.
+ */
+ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
+ // Modify settings that were set before https://www.drupal.org/node/2896115.
+ if(isset($settings['edit_mode']) && $settings['edit_mode'] === 'preview') {
+ $settings['edit_mode'] = 'closed';
+ $settings['closed_mode'] = 'preview';
+ }
+
+ parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+ }
+
/**
* {@inheritdoc}
*/
'title' => t('Paragraph'),
'title_plural' => t('Paragraphs'),
'edit_mode' => 'open',
+ 'closed_mode' => 'summary',
+ 'autocollapse' => 'none',
'add_mode' => 'dropdown',
'form_display_mode' => 'default',
'default_paragraph_type' => '',
$elements['edit_mode'] = array(
'#type' => 'select',
'#title' => $this->t('Edit mode'),
- '#description' => $this->t('The mode the paragraph is in by default. Preview will render the paragraph in the preview view mode.'),
- '#options' => array(
- 'open' => $this->t('Open'),
- 'closed' => $this->t('Closed'),
- 'preview' => $this->t('Preview'),
- ),
+ '#description' => $this->t('The mode the paragraph is in by default.'),
+ '#options' => $this->getSettingOptions('edit_mode'),
'#default_value' => $this->getSetting('edit_mode'),
'#required' => TRUE,
);
+ $elements['closed_mode'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Closed mode'),
+ '#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.'),
+ '#options' => $this->getSettingOptions('closed_mode'),
+ '#default_value' => $this->getSetting('closed_mode'),
+ '#required' => TRUE,
+ ];
+
+ $elements['autocollapse'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Autocollapse'),
+ '#description' => $this->t('When a paragraph is opened for editing, close others.'),
+ '#options' => $this->getSettingOptions('autocollapse'),
+ '#default_value' => $this->getSetting('autocollapse'),
+ '#required' => TRUE,
+ ];
+
$elements['add_mode'] = array(
'#type' => 'select',
'#title' => $this->t('Add mode'),
- '#description' => $this->t('The way to add new paragraphs.'),
- '#options' => array(
- 'select' => $this->t('Select list'),
- 'button' => $this->t('Buttons'),
- 'dropdown' => $this->t('Dropdown button')
- ),
+ '#description' => $this->t('The way to add new Paragraphs.'),
+ '#options' => $this->getSettingOptions('add_mode'),
'#default_value' => $this->getSetting('add_mode'),
'#required' => TRUE,
);
return $elements;
}
+ /**
+ * Returns select options for a plugin setting.
+ *
+ * This is done to allow
+ * \Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget::settingsSummary()
+ * to access option labels. Not all plugin setting are available.
+ *
+ * @param string $setting_name
+ * The name of the widget setting. Supported settings:
+ * - "edit_mode"
+ * - "closed_mode"
+ * - "autocollapse"
+ * - "add_mode"
+ *
+ * @return array|null
+ * An array of setting option usable as a value for a "#options" key.
+ */
+ protected function getSettingOptions($setting_name) {
+ switch($setting_name) {
+ case 'edit_mode':
+ $options = [
+ 'open' => $this->t('Open'),
+ 'closed' => $this->t('Closed'),
+ ];
+ break;
+ case 'closed_mode':
+ $options = [
+ 'summary' => $this->t('Summary'),
+ 'preview' => $this->t('Preview'),
+ ];
+ break;
+ case 'autocollapse':
+ $options = [
+ 'none' => $this->t('None'),
+ 'all' => $this->t('All'),
+ ];
+ break;
+ case 'add_mode':
+ $options = [
+ 'select' => $this->t('Select list'),
+ 'button' => $this->t('Buttons'),
+ 'dropdown' => $this->t('Dropdown button'),
+ 'modal' => $this->t('Modal form'),
+ ];
+ break;
+ }
+
+ return isset($options) ? $options : NULL;
+ }
+
/**
* {@inheritdoc}
*/
'@title_plural' => $this->getSetting('title_plural')
]);
- switch($this->getSetting('edit_mode')) {
- case 'open':
- default:
- $edit_mode = $this->t('Open');
- break;
- case 'closed':
- $edit_mode = $this->t('Closed');
- break;
- case 'preview':
- $edit_mode = $this->t('Preview');
- break;
- }
-
- switch($this->getSetting('add_mode')) {
- case 'select':
- default:
- $add_mode = $this->t('Select list');
- break;
- case 'button':
- $add_mode = $this->t('Buttons');
- break;
- case 'dropdown':
- $add_mode = $this->t('Dropdown button');
- break;
- }
+ $edit_mode = $this->getSettingOptions('edit_mode')[$this->getSetting('edit_mode')];
+ $closed_mode = $this->getSettingOptions('closed_mode')[$this->getSetting('closed_mode')];
+ $autocollapse = $this->getSettingOptions('autocollapse')[$this->getSetting('autocollapse')];
+ $add_mode = $this->getSettingOptions('add_mode')[$this->getSetting('add_mode')];
$summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]);
+ $summary[] = $this->t('Closed mode: @closed_mode', ['@closed_mode' => $closed_mode]);
+ $summary[] = $this->t('Autocollapse: @autocollapse', ['@autocollapse' => $autocollapse]);
$summary[] = $this->t('Add mode: @add_mode', ['@add_mode' => $add_mode]);
+
$summary[] = $this->t('Form display mode: @form_display_mode', [
'@form_display_mode' => $this->getSetting('form_display_mode')
]);
$parents = $element['#field_parents'];
$info = [];
+ /** @var \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity */
$paragraphs_entity = NULL;
$host = $items->getEntity();
$widget_state = static::getWidgetState($parents, $field_name, $form_state);
- $entity_manager = \Drupal::entityTypeManager();
+ $entity_type_manager = \Drupal::entityTypeManager();
$target_type = $this->getFieldSetting('target_type');
$item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit';
$default_edit_mode = $this->getSetting('edit_mode');
+ $closed_mode_setting = isset($widget_state['closed_mode']) ? $widget_state['closed_mode'] : $this->getSetting('closed_mode');
+ $autocollapse_setting = isset($widget_state['autocollapse']) ? $widget_state['autocollapse'] : $this->getSetting('autocollapse');
+
$show_must_be_saved_warning = !empty($widget_state['paragraphs'][$delta]['show_warning']);
if (isset($widget_state['paragraphs'][$delta]['entity'])) {
elseif ($default_edit_mode == 'closed') {
$item_mode = 'closed';
}
- elseif ($default_edit_mode == 'preview') {
- $item_mode = 'preview';
- }
}
}
elseif (isset($widget_state['selected_bundle'])) {
- $entity_type = $entity_manager->getDefinition($target_type);
+ $entity_type = $entity_type_manager->getDefinition($target_type);
$bundle_key = $entity_type->getKey('bundle');
- $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array(
+ $paragraphs_entity = $entity_type_manager->getStorage($target_type)->create(array(
$bundle_key => $widget_state['selected_bundle'],
));
foreach ($violations as $violation) {
$messages[] = $violation->getMessage();
}
- $info['validation_error'] = array(
- '#type' => 'container',
- '#markup' => $this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- );
+ $info['validation_error'] = $this->createMessage($this->t('@messages', ['@messages' => strip_tags(implode('\n', $messages))]));
}
}
$entity_langcode = $paragraphs_entity->language()->getId();
$source = $form_state->get(['content_translation', 'source']);
$source_langcode = $source ? $source->getId() : $entity_langcode;
- $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
+ // Make sure the source language version is used if available. It is a
+ // the host and fetching the translation without this check could lead
+ // valid scenario to have no paragraphs items in the source version of
+ // to an exception.
+ if ($paragraphs_entity->hasTranslation($source_langcode)) {
+ $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
+ }
// The paragraphs entity has no content translation source field if
// no paragraph entity field is translatable, even if the host is.
if ($paragraphs_entity->hasField('content_translation_source')) {
if (isset($item_bundles[$paragraphs_entity->bundle()])) {
$bundle_info = $item_bundles[$paragraphs_entity->bundle()];
- $element['top'] = array(
+ // Create top section structure with all needed subsections.
+ $element['top'] = [
'#type' => 'container',
'#weight' => -1000,
- '#attributes' => array(
- 'class' => array(
- 'paragraph-type-top',
- ),
- ),
- );
-
- $element['top']['paragraph_type_title'] = array(
- '#type' => 'container',
- '#weight' => 0,
- '#attributes' => array(
- 'class' => array(
- 'paragraph-type-title',
- ),
- ),
- );
+ '#attributes' => ['class' => ['paragraph-type-top']],
+ // Section for paragraph type information.
+ 'type' => [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['paragraph-type-title']],
+ 'label' => ['#markup' => $bundle_info['label']],
+ ],
+ // Section for information icons.
+ 'info' => [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['paragraph-type-info']],
+ ],
+ 'summary' => [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['paragraph-type-summary']],
+ ],
+ // Paragraphs actions element for actions and dropdown actions.
+ 'actions' => [
+ '#type' => 'paragraphs_actions',
+ ],
+ ];
- $element['top']['paragraph_type_title']['info'] = array(
- '#markup' => $bundle_info['label'],
- );
+ // Type icon and label bundle.
+ if ($icon_url = $paragraphs_entity->type->entity->getIconUrl()) {
+ $element['top']['type']['icon'] = [
+ '#theme' => 'image',
+ '#uri' => $icon_url,
+ '#attributes' => [
+ 'class' => ['paragraph-type-icon'],
+ 'title' => $bundle_info['label'],
+ ],
+ '#weight' => 0,
+ // We set inline height and width so icon don't resize on first load
+ // while CSS is still not loaded.
+ '#height' => 16,
+ '#width' => 16,
+ ];
+ }
+ $element['top']['type']['label'] = [
+ '#markup' => '<span class="paragraph-type-label">' . $bundle_info['label'] . '</span>',
+ '#weight' => 1,
+ ];
- $actions = [];
- $links = [];
+ // Widget actions.
+ $widget_actions = [
+ 'actions' => [],
+ 'dropdown_actions' => [],
+ ];
- $links['duplicate_button'] = [
+ $widget_actions['dropdown_actions']['duplicate_button'] = [
'#type' => 'submit',
'#value' => $this->t('Duplicate'),
- '#name' => strtr($id_prefix, '-', '_') . '_duplicate',
+ '#name' => $id_prefix . '_duplicate',
'#weight' => 502,
'#submit' => [[get_class($this), 'duplicateSubmit']],
'#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
'#ajax' => [
'callback' => [get_class($this), 'itemAjax'],
'wrapper' => $widget_state['ajax_wrapper_id'],
- 'effect' => 'fade',
],
'#access' => $paragraphs_entity->access('update'),
- '#prefix' => '<li class="duplicate">',
- '#suffix' => '</li>',
];
- // Hide the button when translating.
- $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
- if($item_mode != 'remove') {
- $links['remove_button'] = [
+ if ($item_mode != 'remove') {
+ $widget_actions['dropdown_actions']['remove_button'] = [
'#type' => 'submit',
'#value' => $this->t('Remove'),
- '#name' => strtr($id_prefix, '-', '_') . '_remove',
- '#weight' => 501 ,
+ '#name' => $id_prefix . '_remove',
+ '#weight' => 501,
'#submit' => [[get_class($this), 'paragraphsItemSubmit']],
'#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
'#delta' => $delta,
'#ajax' => [
'callback' => array(get_class($this), 'itemAjax'),
'wrapper' => $widget_state['ajax_wrapper_id'],
- 'effect' => 'fade',
],
- '#access' => $button_access,
- '#prefix' => '<li class="remove">',
- '#suffix' => '</li>',
+ // Hide the button when translating.
+ '#access' => $paragraphs_entity->access('delete') && !$this->isTranslating,
'#paragraphs_mode' => 'remove',
];
}
if ($item_mode == 'edit') {
-
if (isset($paragraphs_entity)) {
- $links['collapse_button'] = array(
- '#type' => 'submit',
+ $widget_actions['actions']['collapse_button'] = [
'#value' => $this->t('Collapse'),
- '#name' => strtr($id_prefix, '-', '_') . '_collapse',
- '#weight' => 499,
- '#submit' => array(array(get_class($this), 'paragraphsItemSubmit')),
+ '#name' => $id_prefix . '_collapse',
+ '#weight' => 1,
+ '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
+ '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
'#delta' => $delta,
- '#ajax' => array(
- 'callback' => array(get_class($this), 'itemAjax'),
+ '#ajax' => [
+ 'callback' => [get_class($this), 'itemAjax'],
'wrapper' => $widget_state['ajax_wrapper_id'],
- 'effect' => 'fade',
- ),
+ ],
'#access' => $paragraphs_entity->access('update'),
- '#prefix' => '<li class="collapse">',
- '#suffix' => '</li>',
'#paragraphs_mode' => 'closed',
'#paragraphs_show_warning' => TRUE,
- );
+ '#attributes' => [
+ 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
+ 'title' => $this->t('Collapse'),
+ ],
+ ];
}
-
- $info['edit_button_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
- );
-
- $info['remove_button_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
- );
-
- $info['edit_remove_button_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
- );
}
else {
- $element['top']['paragraphs_edit_button_container'] = [
- '#type' => 'container',
+ $widget_actions['actions']['edit_button'] = $this->expandButton([
+ '#type' => 'submit',
+ '#value' => $this->t('Edit'),
+ '#name' => $id_prefix . '_edit',
'#weight' => 1,
+ '#attributes' => ['class' => ['paragraphs-button']],
+ '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
+ '#limit_validation_errors' => [
+ array_merge($parents, [$field_name, 'add_more']),
+ ],
+ '#delta' => $delta,
+ '#ajax' => [
+ 'callback' => [get_class($this), 'itemAjax'],
+ 'wrapper' => $widget_state['ajax_wrapper_id'],
+ ],
+ '#access' => $paragraphs_entity->access('update'),
+ '#paragraphs_mode' => 'edit',
'#attributes' => [
- 'class' => [
- 'paragraphs-edit-button-container',
- ],
+ 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
+ 'title' => $this->t('Edit'),
],
- 'paragraphs_edit_button' => [
- '#type' => 'submit',
- '#value' => $this->t('Edit'),
- '#name' => strtr($id_prefix, '-', '_') . '_edit',
- '#weight' => 500,
- '#attributes' => [
- 'class' => [
- 'paragraphs-edit-button',
- ],
- ],
- '#submit' => [[get_class($this), 'paragraphsItemSubmit']],
- '#limit_validation_errors' => [array_merge($parents, [$field_name, 'add_more'])],
- '#delta' => $delta,
- '#ajax' => [
- 'callback' => [get_class($this), 'itemAjax'],
- 'wrapper' => $widget_state['ajax_wrapper_id'],
- 'effect' => 'fade',
- ],
- '#access' => $paragraphs_entity->access('update'),
- '#paragraphs_mode' => 'edit',
- ]
- ];
+ ]);
+
+ if ($show_must_be_saved_warning && $paragraphs_entity->isChanged()) {
+ $info['changed'] = [
+ '#theme' => 'paragraphs_info_icon',
+ '#message' => $this->t('You have unsaved changes on this @title item.', ['@title' => $this->getSetting('title')]),
+ '#icon' => 'changed',
+ ];
+ }
- if ($show_must_be_saved_warning) {
- $info['must_be_saved_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You have unsaved changes on this @title item.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- );
+ if (!$paragraphs_entity->access('view')) {
+ $info['preview'] = [
+ '#theme' => 'paragraphs_info_icon',
+ '#message' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
+ '#icon' => 'view',
+ ];
}
+ }
- $info['preview_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to view this @title.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- '#access' => !$paragraphs_entity->access('view'),
- );
+ // If update is disabled we will show lock icon in actions section.
+ if (!$paragraphs_entity->access('update')) {
+ $widget_actions['actions']['edit_disabled'] = [
+ '#theme' => 'paragraphs_info_icon',
+ '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
+ '#icon' => 'lock',
+ '#weight' => 1,
+ ];
+ }
- $info['edit_button_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to edit this @title.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- '#access' => !$paragraphs_entity->access('update') && $paragraphs_entity->access('delete'),
- );
+ if (!$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete')) {
+ $info['edit'] = [
+ '#theme' => 'paragraphs_info_icon',
+ '#message' => $this->t('You are not allowed to edit or remove this @title.', ['@title' => $this->getSetting('title')]),
+ '#icon' => 'lock',
+ ];
+ }
+ elseif (!$paragraphs_entity->access('update')) {
+ $info['edit'] = [
+ '#theme' => 'paragraphs_info_icon',
+ '#message' => $this->t('You are not allowed to edit this @title.', ['@title' => $this->getSetting('title')]),
+ '#icon' => 'edit-disabled',
+ ];
+ }
+ elseif (!$paragraphs_entity->access('delete')) {
+ $info['remove'] = [
+ '#theme' => 'paragraphs_info_icon',
+ '#message' => $this->t('You are not allowed to remove this @title.', ['@title' => $this->getSetting('title')]),
+ '#icon' => 'delete-disabled',
+ ];
+ }
- $info['remove_button_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to remove this @title.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- '#access' => !$paragraphs_entity->access('delete') && $paragraphs_entity->access('update'),
- );
+ $context = [
+ 'form' => $form,
+ 'widget' => self::getWidgetState($parents, $field_name, $form_state, $widget_state),
+ 'items' => $items,
+ 'delta' => $delta,
+ 'element' => $element,
+ 'form_state' => $form_state,
+ 'paragraphs_entity' => $paragraphs_entity,
+ ];
- $info['edit_remove_button_info'] = array(
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to edit or remove this @title.', array('@title' => $this->getSetting('title'))),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- '#access' => !$paragraphs_entity->access('update') && !$paragraphs_entity->access('delete'),
- );
- }
+ // Allow modules to alter widget actions.
+ \Drupal::moduleHandler()->alter('paragraphs_widget_actions', $widget_actions, $context);
- if (count($links)) {
- $show_links = 0;
- foreach($links as $link_item) {
- if (!isset($link_item['#access']) || $link_item['#access']) {
- $show_links++;
- }
- }
+ if (count($widget_actions['actions'])) {
+ // Expand all actions to proper submit elements and add it to top
+ // actions sub component.
+ $element['top']['actions']['actions'] = array_map([$this, 'expandButton'], $widget_actions['actions']);
+ }
- if ($show_links > 0) {
-
- $element['top']['links'] = $links;
- if ($show_links > 1) {
- $element['top']['links']['#theme_wrappers'] = array('dropbutton_wrapper', 'paragraphs_dropbutton_wrapper');
- $element['top']['links']['prefix'] = array(
- '#markup' => '<ul class="dropbutton">',
- '#weight' => -999,
- );
- $element['top']['links']['suffix'] = array(
- '#markup' => '</li>',
- '#weight' => 999,
- );
- }
- else {
- $element['top']['links']['#theme_wrappers'] = array('paragraphs_dropbutton_wrapper');
- foreach($links as $key => $link_item) {
- unset($element['top']['links'][$key]['#prefix']);
- unset($element['top']['links'][$key]['#suffix']);
- }
- }
- $element['top']['links']['#weight'] = 1;
- }
+ if (count($widget_actions['dropdown_actions'])) {
+ // Expand all dropdown actions to proper submit elements and add
+ // them to top dropdown actions sub component.
+ $element['top']['actions']['dropdown_actions'] = array_map([$this, 'expandButton'], $widget_actions['dropdown_actions']);
}
if (count($info)) {
- $show_info = FALSE;
- foreach($info as $info_item) {
+ foreach ($info as $info_item) {
if (!isset($info_item['#access']) || $info_item['#access']) {
- $show_info = TRUE;
- break;
- }
- }
-
- if ($show_info) {
- $element['info'] = $info;
- $element['info']['#weight'] = 998;
- }
- }
-
- if (count($actions)) {
- $show_actions = FALSE;
- foreach($actions as $action_item) {
- if (!isset($action_item['#access']) || $action_item['#access']) {
- $show_actions = TRUE;
+ $element['top']['info']['items'] = $info;
break;
}
}
-
- if ($show_actions) {
- $element['actions'] = $actions;
- $element['actions']['#type'] = 'actions';
- $element['actions']['#weight'] = 999;
- }
}
}
if ($item_mode == 'edit') {
$display->buildForm($paragraphs_entity, $element['subform'], $form_state);
+ // Get the field definitions of the paragraphs_entity.
+ // We need them to filter out entity reference revisions fields that
+ // reference paragraphs, cause otherwise we have problems with showing
+ // and hiding the right fields in nested paragraphs.
+ $field_definitions = $paragraphs_entity->getFieldDefinitions();
+
foreach (Element::children($element['subform']) as $field) {
+ // Do a check if we have to add a class to the form element. We need
+ // those classes (paragraphs-content and paragraphs-behavior) to show
+ // and hide elements, depending of the active perspective.
+ $omit_class = FALSE;
+ if (isset($field_definitions[$field])) {
+ $type = $field_definitions[$field]->getType();
+ if ($type == 'entity_reference_revisions') {
+ // Check if we are referencing paragraphs.
+ $target_entity_type = $field_definitions[$field]->get('entity_type');
+ if ($target_entity_type && $target_entity_type == 'paragraph') {
+ $omit_class = TRUE;
+ }
+ }
+ }
+
if ($paragraphs_entity->hasField($field)) {
+ if (!$omit_class) {
+ $element['subform'][$field]['#attributes']['class'][] = 'paragraphs-content';
+ }
$translatable = $paragraphs_entity->{$field}->getFieldDefinition()->isTranslatable();
if ($translatable) {
$element['subform'][$field]['widget']['#after_build'][] = [
static::class,
- 'removeTranslatabilityClue'
+ 'removeTranslatabilityClue',
];
}
}
// Build the behavior plugins fields.
$paragraphs_type = $paragraphs_entity->getParagraphType();
- if ($paragraphs_type) {
+ if ($paragraphs_type && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
+ $element['behavior_plugins']['#weight'] = -99;
foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
- $element['behavior_plugins'][$plugin_id] = [];
+ $element['behavior_plugins'][$plugin_id] = [
+ '#type' => 'container',
+ '#group' => implode('][', array_merge($element_parents, ['paragraph_behavior'])),
+ ];
$subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form, $form_state);
if ($plugin_form = $plugin->buildBehaviorForm($paragraphs_entity, $element['behavior_plugins'][$plugin_id], $subform_state)) {
$element['behavior_plugins'][$plugin_id] = $plugin_form;
+ // Add the paragraphs-behavior class, so that we are able to show
+ // and hide behavior fields, depending on the active perspective.
+ $element['behavior_plugins'][$plugin_id]['#attributes']['class'][] = 'paragraphs-behavior';
}
}
}
}
- elseif ($item_mode == 'preview') {
- $element['subform'] = array();
- $element['behavior_plugins'] = [];
- $element['preview'] = entity_view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
- $element['preview']['#access'] = $paragraphs_entity->access('view');
- }
elseif ($item_mode == 'closed') {
- $element['subform'] = array();
+ $element['subform'] = [];
$element['behavior_plugins'] = [];
- if ($paragraphs_entity) {
- $summary = $this->addCollapsedSummary($paragraphs_entity);
- $element['top']['paragraph_summary']['fields_info'] = [
- '#markup' => $summary,
- '#prefix' => '<div class="paragraphs-collapsed-description">',
- '#suffix' => '</div>',
- ];
+ if ($closed_mode_setting === 'preview') {
+ // The closed paragraph is displayed as a rendered preview.
+ $view_builder = $entity_type_manager->getViewBuilder('paragraph');
+
+ $element['preview'] = $view_builder->view($paragraphs_entity, 'preview', $paragraphs_entity->language()->getId());
+ $element['preview']['#access'] = $paragraphs_entity->access('view');
+ }
+ else {
+ // The closed paragraph is displayed as a summary.
+ if ($paragraphs_entity) {
+ $summary = $paragraphs_entity->getSummary();
+ if (!empty($summary)) {
+ $element['top']['summary']['fields_info'] = [
+ '#markup' => $summary,
+ '#prefix' => '<div class="paragraphs-collapsed-description">',
+ '#suffix' => '</div>',
+ '#access' => $paragraphs_entity->access('view'),
+ ];
+ }
+ }
}
}
else {
$widget_state['paragraphs'][$delta]['entity'] = $paragraphs_entity;
$widget_state['paragraphs'][$delta]['display'] = $display;
$widget_state['paragraphs'][$delta]['mode'] = $item_mode;
+ $widget_state['closed_mode'] = $closed_mode_setting;
+ $widget_state['autocollapse'] = $autocollapse_setting;
static::setWidgetState($parents, $field_name, $form_state, $widget_state);
}
return $element;
}
- public function getAllowedTypes() {
-
- $return_bundles = array();
+ /**
+ * Builds an add paragraph button for opening of modal form.
+ *
+ * @param array $element
+ * Render element.
+ */
+ protected function buildModalAddForm(array &$element) {
+ // Attach the theme for the dialog template.
+ $element['#theme'] = 'paragraphs_add_dialog';
- $target_type = $this->getFieldSetting('target_type');
- $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($target_type);
+ $element['add_modal_form_area'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => [
+ 'paragraph-type-add-modal',
+ 'first-button',
+ ],
+ ],
+ '#access' => !$this->isTranslating,
+ '#weight' => -2000,
+ ];
- if ($this->getSelectionHandlerSetting('target_bundles') !== NULL) {
- $bundles = array_intersect_key($bundles, $this->getSelectionHandlerSetting('target_bundles'));
- }
+ $element['add_modal_form_area']['add_more'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Add @title', ['@title' => $this->getSetting('title')]),
+ '#name' => 'button_add_modal',
+ '#attributes' => [
+ 'class' => [
+ 'paragraph-type-add-modal-button',
+ 'js-show',
+ ],
+ ],
+ ];
- // Support for the paragraphs reference type.
- $drag_drop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
- if ($drag_drop_settings) {
- $max_weight = count($bundles);
+ $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.modal';
+ }
- foreach ($drag_drop_settings as $bundle_info) {
- if (isset($bundle_info['weight']) && $bundle_info['weight'] && $bundle_info['weight'] > $max_weight) {
- $max_weight = $bundle_info['weight'];
- }
- }
+ /**
+ * Returns the sorted allowed types for a entity reference field.
+ *
+ * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+ * (optional) The field definition forwhich the allowed types should be
+ * returned, defaults to the current field.
+ *
+ * @return array
+ * A list of arrays keyed by the paragraph type machine name with the following properties.
+ * - label: The label of the paragraph type.
+ * - weight: The weight of the paragraph type.
+ */
+ public function getAllowedTypes(FieldDefinitionInterface $field_definition = NULL) {
- // Default weight for new items.
- $weight = $max_weight + 1;
- foreach ($bundles as $machine_name => $bundle) {
- $return_bundles[$machine_name] = array(
- 'label' => $bundle['label'],
- 'weight' => isset($drag_drop_settings[$machine_name]['weight']) ? $drag_drop_settings[$machine_name]['weight'] : $weight,
- );
- $weight++;
- }
+ $return_bundles = array();
+ /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
+ $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
+ $handler = $selection_manager->getSelectionHandler($field_definition ?: $this->fieldDefinition);
+ if ($handler instanceof ParagraphSelection) {
+ $return_bundles = $handler->getSortedAllowedTypes();
}
// Support for other reference types.
else {
+ $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($field_definition ? $field_definition->getSetting('target_type') : $this->fieldDefinition->getSetting('target_type'));
$weight = 0;
foreach ($bundles as $machine_name => $bundle) {
if (!count($this->getSelectionHandlerSetting('target_bundles'))
}
}
- uasort($return_bundles, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
return $return_bundles;
}
$this->realItemCount = $max;
$is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
- $title = $this->fieldDefinition->getLabel();
+ $field_title = $this->fieldDefinition->getLabel();
$description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
$elements = array();
+ $tabs = '';
$this->fieldIdPrefix = implode('-', array_merge($this->fieldParents, array($field_name)));
$this->fieldWrapperId = Html::getUniqueId($this->fieldIdPrefix . '-add-more-wrapper');
- $elements['#prefix'] = '<div id="' . $this->fieldWrapperId . '">';
+
+ // If the parent entity is paragraph add the nested class if not then add
+ // the perspective tabs.
+ $field_prefix = strtr($this->fieldIdPrefix, '_', '-');
+ if (count($this->fieldParents) == 0) {
+ if ($items->getEntity()->getEntityTypeId() != 'paragraph') {
+ $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>';
+ }
+ }
+ if (count($this->fieldParents) > 0) {
+ if ($items->getEntity()->getEntityTypeId() === 'paragraph') {
+ $form['#attributes']['class'][] = 'paragraphs-nested';
+ }
+ }
+ $elements['#prefix'] = '<div class="is-horizontal paragraphs-tabs-wrapper" id="' . $this->fieldWrapperId . '">' . $tabs;
$elements['#suffix'] = '</div>';
$field_state['ajax_wrapper_id'] = $this->fieldWrapperId;
// Persist the widget state so formElement() can access it.
static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
+ $header_actions = $this->buildHeaderActions($field_state, $form_state);
+ if ($header_actions) {
+ $elements['header_actions'] = $header_actions;
+ // Add a weight element so we guaranty that header actions will stay in
+ // first row. We will use this later in
+ // paragraphs_preprocess_field_multiple_value_form().
+ $elements['header_actions']['_weight'] = [
+ '#type' => 'weight',
+ '#default_value' => -100,
+ ];
+ }
+
+ if (!empty($field_state['dragdrop'])) {
+ $elements['#attached']['library'][] = 'paragraphs/paragraphs-dragdrop';
+ //$elements['dragdrop_mode']['#button_type'] = 'primary';
+ $elements['dragdrop'] = $this->buildNestedParagraphsFoDragDrop($form_state, NULL, []);
+ return $elements;
+ }
+
if ($max > 0) {
for ($delta = 0; $delta < $max; $delta++) {
// For multiple fields, title and description are handled by the wrapping
// table.
$element = array(
- '#title' => $is_multiple ? '' : $title,
+ '#title' => $is_multiple ? '' : $field_title,
'#description' => $is_multiple ? '' : $description,
);
$element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
$field_state = static::getWidgetState($this->fieldParents, $field_name, $form_state);
$field_state['real_item_count'] = $this->realItemCount;
+ $field_state['add_mode'] = $this->getSetting('add_mode');
static::setWidgetState($this->fieldParents, $field_name, $form_state, $field_state);
+ $elements += [
+ '#element_validate' => [[$this, 'multipleElementValidate']],
+ '#required' => $this->fieldDefinition->isRequired(),
+ '#field_name' => $field_name,
+ '#cardinality' => $cardinality,
+ '#max_delta' => $max - 1,
+ ];
+
if ($this->realItemCount > 0) {
$elements += array(
'#theme' => 'field_multiple_value_form',
- '#field_name' => $field_name,
- '#cardinality' => $cardinality,
'#cardinality_multiple' => $is_multiple,
- '#required' => $this->fieldDefinition->isRequired(),
- '#title' => $title,
+ '#title' => $field_title,
'#description' => $description,
- '#max_delta' => $max-1,
);
+
}
else {
+ $classes = $this->fieldDefinition->isRequired() ? ['form-required'] : [];
$elements += [
'#type' => 'container',
'#theme_wrappers' => ['container'],
- '#field_name' => $field_name,
- '#cardinality' => $cardinality,
'#cardinality_multiple' => TRUE,
- '#max_delta' => $max-1,
'title' => [
'#type' => 'html_tag',
'#tag' => 'strong',
- '#value' => $title,
+ '#value' => $field_title,
+ '#attributes' => ['class' => $classes],
],
'text' => [
'#type' => 'container',
}
/**
- * Add 'add more' button, if not working with a programmed form.
+ * Returns a list of child paragraphs for a given field to loop over.
*
- * @return array
- * The form element array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param string $field_name
+ * The field name for which to find child paragraphs.
+ * @param \Drupal\paragraphs\ParagraphInterface $paragraph
+ * The current paragraph.
+ * @param array $array_parents
+ * The current field parent structure.
+ *
+ * @return \Drupal\paragraphs\Entity\Paragraph[]
+ * Child paragraphs.
*/
- protected function buildAddActions() {
- if (count($this->getAccessibleOptions()) === 0) {
- if (count($this->getAllowedTypes()) === 0) {
- $add_more_elements['info'] = [
- '#type' => 'container',
- '#markup' => $this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- ];
- }
- else {
- $add_more_elements['info'] = [
- '#type' => 'container',
- '#markup' => $this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]),
- '#attributes' => ['class' => ['messages', 'messages--warning']],
- ];
+ protected function getChildParagraphs(FormStateInterface $form_state, $field_name, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
+
+ // Convert the parents structure which only includes field names and delta
+ // to the full storage array key which includes a prefix and a subform.
+ $full_parents_key = ['field_storage', '#parents'];
+ foreach ($array_parents as $i => $parent) {
+ $full_parents_key[] = $parent;
+ if ($i % 2) {
+ $full_parents_key[] = 'subform';
}
-
- return $add_more_elements ;
}
- if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') {
- return $this->buildButtonsAddMode();
+ $current_parents = array_merge($full_parents_key, ['#fields', $field_name]);
+ $child_field_state = NestedArray::getValue($form_state->getStorage(), $current_parents);
+ $entities = [];
+ if ($child_field_state && isset($child_field_state['paragraphs'])) {
+ // Fetch the paragraphs from the field state. Use the original delta
+ // to get the right position. Also reorder the paragraphs in the widget
+ // state accordingly.
+ $new_widget_paragraphs = [];
+ foreach ($child_field_state['paragraphs'] as $child_delta => $child_field_item_state) {
+ $entities[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state['entity'];
+ $new_widget_paragraphs[array_search($child_delta, $child_field_state['original_deltas'])] = $child_field_item_state;
+ }
+ ksort($entities);
+
+ // Set the orderd paragraphs into the widget state and reset original
+ // deltas.
+ ksort($new_widget_paragraphs);
+ $child_field_state['paragraphs'] = $new_widget_paragraphs;
+ $child_field_state['original_deltas'] = range(0, count($child_field_state['paragraphs']) - 1);
+ NestedArray::setValue($form_state->getStorage(), $current_parents, $child_field_state);
+ }
+ elseif ($paragraph) {
+ // If there is no field state, return the paragraphs directly from the
+ // entity.
+ foreach ($paragraph->get($field_name) as $child_delta => $item) {
+ if ($item->entity) {
+ $entities[$child_delta] = $item->entity;
+ }
+ }
}
- return $this->buildSelectAddMode();
+ return $entities;
}
/**
- * Returns the available paragraphs type.
+ * Builds the nested drag and drop structure.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param \Drupal\paragraphs\ParagraphInterface|null $paragraph
+ * The parent paragraph, NULL for the initial call.
+ * @param string[] $array_parents
+ * The array parents for nested paragraphs.
*
* @return array
- * Available paragraphs types.
+ * The built form structure.
*/
- protected function getAccessibleOptions() {
- if ($this->accessOptions !== NULL) {
- return $this->accessOptions;
- }
-
- $entity_type_manager = \Drupal::entityTypeManager();
- $target_type = $this->getFieldSetting('target_type');
- $bundles = $this->getAllowedTypes();
- $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
- $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
-
- foreach ($bundles as $machine_name => $bundle) {
- if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
- || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
- if ($access_control_handler->createAccess($machine_name)) {
- $this->accessOptions[$machine_name] = $bundle['label'];
+ protected function buildNestedParagraphsFoDragDrop(FormStateInterface $form_state, ParagraphInterface $paragraph = NULL, array $array_parents = []) {
+ // Look for nested elements.
+ $elements = [];
+ $field_definitions = [];
+ if ($paragraph) {
+ foreach ($paragraph->getFieldDefinitions() as $child_field_name => $field_definition) {
+ /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
+ if ($field_definition->getType() == 'entity_reference_revisions' && $field_definition->getSetting('target_type') == 'paragraph') {
+ $field_definitions[$child_field_name] = $field_definition;
+ }
+ }
+ }
+ else {
+ $field_definitions = [$this->fieldDefinition->getName() => $this->fieldDefinition];
+ }
+
+ /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
+ foreach ($field_definitions as $child_field_name => $field_definition) {
+ $child_path = implode('][', array_merge($array_parents, [$child_field_name]));
+ $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality();
+ $allowed_types = implode(array_keys($this->getAllowedTypes($field_definition)), ',');
+ $elements[$child_field_name] = [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['paragraphs-dragdrop-wrapper']],
+ ];
+
+ // Only show a field label if there is more than one paragraph field.
+ $label = count($field_definitions) > 1 || !$paragraph ? '<label><strong>' . $field_definition->getLabel() . '</strong></label>' : '';
+
+ $elements[$child_field_name]['list'] = [
+ '#type' => 'markup',
+ '#prefix' => $label . '<ul class="paragraphs-dragdrop" data-paragraphs-dragdrop-cardinality="' . $cardinality . '" data-paragraphs-dragdrop-allowed-types="' . $allowed_types . '" data-paragraphs-dragdrop-path="' . $child_path . '">',
+ '#suffix' => '</ul>',
+ ];
+
+ /** @var \Drupal\paragraphs\Entity\Paragraph $child_paragraph */
+ foreach ($this->getChildParagraphs($form_state, $child_field_name, $paragraph, $array_parents) as $child_delta => $child_paragraph) {
+ $element = [];
+ $element['top'] = [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['paragraphs-summary-wrapper']],
+ ];
+ $element['top']['paragraph_summary']['type'] = [
+ '#markup' => '<strong>' . $child_paragraph->getParagraphType()->label() . '</strong>',
+ ];
+
+ // We name the element '_weight' to avoid clashing with elements
+ // defined by widget.
+ $element['_weight'] = array(
+ '#type' => 'hidden',
+ '#default_value' => $child_delta,
+ '#attributes' => [
+ 'class' => ['paragraphs-dragdrop__weight'],
+ ]
+ );
+
+ $element['_path'] = [
+ '#type' => 'hidden',
+ '#title' => $this->t('Current path for @number', ['@number' => $delta = 1]),
+ '#title_display' => 'invisible',
+ '#default_value' => $child_path,
+ '#attributes' => [
+ 'class' => ['paragraphs-dragdrop__path'],
+ ]
+ ];
+
+ $summary_options = [];
+
+ $element['#prefix'] = '<li data-paragraphs-dragdrop-bundle="' . $child_paragraph->bundle() . '"><a href="#" class="tabledrag-handle"><div class="handle"> </div></a>';
+ $element['#suffix'] = '</li>';
+ $child_array_parents = array_merge($array_parents, [$child_field_name, $child_delta]);
+
+ if ($child_elements = $this->buildNestedParagraphsFoDragDrop($form_state, $child_paragraph, $child_array_parents)) {
+ $element['dragdrop'] = $child_elements;
+
+ // Set the depth limit to 0 to avoid displaying a summary for the
+ // children.
+ $summary_options['depth_limit'] = 0;
+ }
+
+ $element['top']['summary']['fields_info'] = [
+ '#markup' => $child_paragraph->getSummary($summary_options),
+ '#prefix' => '<div class="paragraphs-collapsed-description">',
+ '#suffix' => '</div>',
+ ];
+
+ $elements[$child_field_name]['list'][$child_delta] = $element;
+ }
+ }
+ return $elements;
+ }
+
+ /**
+ * Add 'add more' button, if not working with a programmed form.
+ *
+ * @return array
+ * The form element array.
+ */
+ protected function buildAddActions() {
+ if (count($this->getAccessibleOptions()) === 0) {
+ if (count($this->getAllowedTypes()) === 0) {
+ $add_more_elements['info'] = $this->createMessage($this->t('You are not allowed to add any of the @title types.', ['@title' => $this->getSetting('title')]));
+ }
+ else {
+ $add_more_elements['info'] = $this->createMessage($this->t('You did not add any @title types yet.', ['@title' => $this->getSetting('title')]));
+ }
+
+ return $add_more_elements;
+ }
+
+ if (in_array($this->getSetting('add_mode'), ['button', 'dropdown', 'modal'])) {
+ return $this->buildButtonsAddMode();
+ }
+
+ return $this->buildSelectAddMode();
+ }
+
+ /**
+ * Returns the available paragraphs type.
+ *
+ * @return array
+ * Available paragraphs types.
+ */
+ protected function getAccessibleOptions() {
+ if ($this->accessOptions !== NULL) {
+ return $this->accessOptions;
+ }
+
+ $entity_type_manager = \Drupal::entityTypeManager();
+ $target_type = $this->getFieldSetting('target_type');
+ $bundles = $this->getAllowedTypes();
+ $access_control_handler = $entity_type_manager->getAccessControlHandler($target_type);
+ $dragdrop_settings = $this->getSelectionHandlerSetting('target_bundles_drag_drop');
+
+ foreach ($bundles as $machine_name => $bundle) {
+ if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
+ || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
+ if ($access_control_handler->createAccess($machine_name)) {
+ $this->accessOptions[$machine_name] = $bundle['label'];
}
}
}
}
/**
- * Builds dropdown button for adding new paragraph.
+ * Helper to create a paragraph UI message.
+ *
+ * @param string $message
+ * Message text.
+ * @param string $type
+ * Message type.
*
* @return array
- * The form element array.
+ * Render array of message.
*/
- protected function buildButtonsAddMode() {
- // Hide the button when translating.
- $add_more_elements = [
+ public function createMessage($message, $type = 'warning') {
+ return [
'#type' => 'container',
- '#theme_wrappers' => ['paragraphs_dropbutton_wrapper'],
+ '#markup' => $message,
+ '#attributes' => ['class' => ['messages', 'messages--' . $type]],
];
- $field_name = $this->fieldDefinition->getName();
- $title = $this->fieldDefinition->getLabel();
-
- $drop_button = FALSE;
- if (count($this->getAccessibleOptions()) > 1 && $this->getSetting('add_mode') == 'dropdown') {
- $drop_button = TRUE;
- $add_more_elements['#theme_wrappers'] = ['dropbutton_wrapper'];
- $add_more_elements['prefix'] = [
- '#markup' => '<ul class="dropbutton">',
- '#weight' => -999,
- ];
- $add_more_elements['suffix'] = [
- '#markup' => '</ul>',
- '#weight' => 999,
+ }
+
+ /**
+ * Expand button base array into a paragraph widget action button.
+ *
+ * @param array $button_base
+ * Button base render array.
+ *
+ * @return array
+ * Button render array.
+ */
+ public static function expandButton(array $button_base) {
+ // Do not expand elements that do not have submit handler.
+ if (empty($button_base['#submit'])) {
+ return $button_base;
+ }
+
+ $button = $button_base + [
+ '#type' => 'submit',
+ '#theme_wrappers' => ['input__submit__paragraph_action'],
+ ];
+
+ // Html::getId will give us '-' char in name but we want '_' for now so
+ // we use strtr to search&replace '-' to '_'.
+ $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_');
+ $button['#id'] = Html::getUniqueId($button['#name']);
+
+ if (isset($button['#ajax'])) {
+ $button['#ajax'] += [
+ 'effect' => 'fade',
+ // Since a normal throbber is added inline, this has the potential to
+ // break a layout if the button is located in dropbuttons. Instead,
+ // it's safer to just show the fullscreen progress element instead.
+ 'progress' => ['type' => 'fullscreen'],
];
- $add_more_elements['#suffix'] = $this->t(' to %type', ['%type' => $title]);
}
- foreach ($this->getAccessibleOptions() as $machine_name => $label) {
- $add_more_elements['add_more_button_' . $machine_name] = [
+ return $button;
+ }
+
+ /**
+ * Get common submit element information for processing ajax submit handlers.
+ *
+ * @param array $form
+ * Form array.
+ * @param FormStateInterface $form_state
+ * Form state object.
+ * @param int $position
+ * Position of triggering element.
+ *
+ * @return array
+ * Submit element information.
+ */
+ public static function getSubmitElementInfo(array $form, FormStateInterface $form_state, $position = ParagraphsWidget::ACTION_POSITION_BASE) {
+ $submit['button'] = $form_state->getTriggeringElement();
+
+ // Go up in the form, to the widgets container.
+ if ($position == ParagraphsWidget::ACTION_POSITION_BASE) {
+ $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -2));
+ }
+ if ($position == ParagraphsWidget::ACTION_POSITION_HEADER) {
+ $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -3));
+ }
+ elseif ($position == ParagraphsWidget::ACTION_POSITION_ACTIONS) {
+ $submit['element'] = NestedArray::getValue($form, array_slice($submit['button']['#array_parents'], 0, -5));
+ $delta = array_slice($submit['button']['#array_parents'], -5, -4);
+ $submit['delta'] = $delta[0];
+ }
+
+ $submit['field_name'] = $submit['element']['#field_name'];
+ $submit['parents'] = $submit['element']['#field_parents'];
+
+ // Get widget state.
+ $submit['widget_state'] = static::getWidgetState($submit['parents'], $submit['field_name'], $form_state);
+
+ return $submit;
+ }
+
+ /**
+ * Build drop button.
+ *
+ * @param array $elements
+ * Elements for drop button.
+ *
+ * @return array
+ * Drop button array.
+ */
+ protected function buildDropbutton(array $elements = []) {
+ $build = [
+ '#type' => 'container',
+ '#attributes' => ['class' => ['paragraphs-dropbutton-wrapper']],
+ ];
+
+ $operations = [];
+ // Because we are cloning the elements into title sub element we need to
+ // sort children first.
+ foreach (Element::children($elements, TRUE) as $child) {
+ // Clone the element as an operation.
+ $operations[$child] = ['title' => $elements[$child]];
+
+ // Flag the original element as printed so it doesn't render twice.
+ $elements[$child]['#printed'] = TRUE;
+ }
+
+ $build['operations'] = [
+ '#type' => 'paragraph_operations',
+ // Even though operations are run through the "links" element type, the
+ // theme system will render any render array passed as a link "title".
+ '#links' => $operations,
+ ];
+
+ return $build + $elements;
+ }
+
+ /**
+ * Builds dropdown button for adding new paragraph.
+ *
+ * @return array
+ * The form element array.
+ */
+ protected function buildButtonsAddMode() {
+ $options = $this->getAccessibleOptions();
+ $add_mode = $this->getSetting('add_mode');
+ $paragraphs_type_storage = \Drupal::entityTypeManager()->getStorage('paragraphs_type');
+
+ // Build the buttons.
+ $add_more_elements = [];
+ foreach ($options as $machine_name => $label) {
+ $button_key = 'add_more_button_' . $machine_name;
+ $add_more_elements[$button_key] = $this->expandButton([
'#type' => 'submit',
- '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_' . $machine_name . '_add_more',
- '#value' => $this->t('Add @type', ['@type' => $label]),
+ '#name' => $this->fieldIdPrefix . '_' . $machine_name . '_add_more',
+ '#value' => $add_mode == 'modal' ? $label : $this->t('Add @type', ['@type' => $label]),
'#attributes' => ['class' => ['field-add-more-submit']],
- '#limit_validation_errors' => [array_merge($this->fieldParents, [$field_name, 'add_more'])],
+ '#limit_validation_errors' => [array_merge($this->fieldParents, [$this->fieldDefinition->getName(), 'add_more'])],
'#submit' => [[get_class($this), 'addMoreSubmit']],
'#ajax' => [
'callback' => [get_class($this), 'addMoreAjax'],
'wrapper' => $this->fieldWrapperId,
- 'effect' => 'fade',
],
'#bundle_machine_name' => $machine_name,
- ];
+ ]);
- if ($drop_button) {
- $add_more_elements['add_more_button_' . $machine_name]['#prefix'] = '<li>';
- $add_more_elements['add_more_button_' . $machine_name]['#suffix'] = '</li>';
+ if ($add_mode === 'modal' && $icon_url = $paragraphs_type_storage->load($machine_name)->getIconUrl()) {
+ $add_more_elements[$button_key]['#attributes']['style'] = 'background-image: url(' . $icon_url . ');';
}
}
+ // Determine if buttons should be rendered as dropbuttons.
+ if (count($options) > 1 && $add_mode == 'dropdown') {
+ $add_more_elements = $this->buildDropbutton($add_more_elements);
+ $add_more_elements['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
+ }
+ elseif ($add_mode == 'modal') {
+ $this->buildModalAddForm($add_more_elements);
+ $add_more_elements['add_modal_form_area']['#suffix'] = $this->t('to %type', ['%type' => $this->fieldDefinition->getLabel()]);
+ }
+ $add_more_elements['#weight'] = 1;
+
return $add_more_elements;
}
*/
protected function buildSelectAddMode() {
$field_name = $this->fieldDefinition->getName();
- $title = $this->fieldDefinition->getLabel();
+ $field_title = $this->fieldDefinition->getLabel();
+ $setting_title = $this->getSetting('title');
$add_more_elements['add_more_select'] = [
'#type' => 'select',
'#options' => $this->getAccessibleOptions(),
- '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]),
+ '#title' => $this->t('@title type', ['@title' => $setting_title]),
'#label_display' => 'hidden',
];
- $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]);
+ $text = $this->t('Add @title', ['@title' => $setting_title]);
if ($this->realItemCount > 0) {
- $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]);
+ $text = $this->t('Add another @title', ['@title' => $setting_title]);
}
$add_more_elements['add_more_button'] = [
],
];
- $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]);
+ $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $field_title]);
return $add_more_elements;
}
* {@inheritdoc}
*/
public static function addMoreAjax(array $form, FormStateInterface $form_state) {
- $button = $form_state->getTriggeringElement();
- // Go one level up in the form, to the widgets container.
- $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
+ $element = $submit['element'];
// Add a DIV around the delta receiving the Ajax effect.
- $delta = $element['#max_delta'];
+ $delta = $submit['element']['#max_delta'];
$element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
$element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
}
/**
- * {@inheritdoc}
+ * Ajax callback for all actions.
*/
- public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
- $button = $form_state->getTriggeringElement();
+ public static function allActionsAjax(array $form, FormStateInterface $form_state) {
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+ $element = $submit['element'];
- // Go one level up in the form, to the widgets container.
- $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
- $field_name = $element['#field_name'];
- $parents = $element['#field_parents'];
+ // Add a DIV around the delta receiving the Ajax effect.
+ $delta = $submit['element']['#max_delta'];
+ $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
+ $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
- // Increment the items count.
- $widget_state = static::getWidgetState($parents, $field_name, $form_state);
+ return $element;
+ }
- if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
- $widget_state['items_count']++;
+ /**
+ * {@inheritdoc}
+ */
+ public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state);
+
+ if ($submit['widget_state']['real_item_count'] < $submit['element']['#cardinality'] || $submit['element']['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
+ $submit['widget_state']['items_count']++;
}
- if (isset($button['#bundle_machine_name'])) {
- $widget_state['selected_bundle'] = $button['#bundle_machine_name'];
+ if (isset($submit['button']['#bundle_machine_name'])) {
+ $submit['widget_state']['selected_bundle'] = $submit['button']['#bundle_machine_name'];
}
else {
- $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value'];
+ $submit['widget_state']['selected_bundle'] = $submit['element']['add_more']['add_more_select']['#value'];
}
- static::setWidgetState($parents, $field_name, $form_state, $widget_state);
+ $submit['widget_state'] = static::autocollapse($submit['widget_state']);
+
+ static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
$form_state->setRebuild();
}
public static function duplicateSubmit(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
// Go one level up in the form, to the widgets container.
- $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
+ $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -5));
$field_name = $element['#field_name'];
$parents = $element['#field_parents'];
// Inserting new element in the array.
$widget_state = static::getWidgetState($parents, $field_name, $form_state);
- $delta = $button['#delta'];
+
+ // Map the button delta to the actual delta.
+ $original_button_delta = $button['#delta'];
+ $current_button_delta = array_search($button['#delta'], $widget_state['original_deltas']);
+
$widget_state['items_count']++;
$widget_state['real_item_count']++;
- $widget_state['original_deltas'] = array_merge($widget_state['original_deltas'], ['1' => 1]) ;
- // Check if the replicate module is enabled
+ // Initialize the new original delta map with the new entry.
+ $new_original_deltas = [
+ $current_button_delta + 1 => count($widget_state['original_deltas']),
+ ];
+
+ $user_input = NestedArray::getValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5));
+ $user_input[count($widget_state['original_deltas'])]['_weight'] = $current_button_delta + 1;
+
+ // Increase all original deltas bigger than the delta of the duplicated
+ // element by one.
+ foreach ($widget_state['original_deltas'] as $current_delta => $original_delta) {
+ $new_delta = $current_delta > $current_button_delta ? $current_delta + 1 : $current_delta;
+ $new_original_deltas[$new_delta] = $original_delta;
+ $user_input[$original_delta]['_weight'] = $new_delta;
+ }
+ $widget_state['original_deltas'] = $new_original_deltas;
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
+ $entity = $widget_state['paragraphs'][$original_button_delta]['entity'];
+
+ $widget_state = static::autocollapse($widget_state);
+
+ // Check if the replicate module is enabled.
if (\Drupal::hasService('replicate.replicator')) {
- $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($widget_state['paragraphs'][$delta]['entity']);
- }
+ $duplicate_entity = \Drupal::getContainer()->get('replicate.replicator')->replicateEntity($entity);
+ }
else {
- $duplicate_entity = $widget_state['paragraphs'][$delta]['entity']->createDuplicate();
- }
+ $duplicate_entity = $entity->createDuplicate();
+ }
// Create the duplicated paragraph and insert it below the original.
- $paragraph[] = [
+ $widget_state['paragraphs'][] = [
'entity' => $duplicate_entity,
- 'display' => $widget_state['paragraphs'][$delta]['display'],
- 'mode' => 'edit'
+ 'display' => $widget_state['paragraphs'][$original_button_delta]['display'],
+ 'mode' => 'edit',
];
- array_splice($widget_state['paragraphs'], $delta + 1, 0, $paragraph);
-
+ NestedArray::setValue($form_state->getUserInput(), array_slice($button['#parents'], 0, -5), $user_input);
static::setWidgetState($parents, $field_name, $form_state, $widget_state);
$form_state->setRebuild();
}
public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) {
- $button = $form_state->getTriggeringElement();
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
- // Go one level up in the form, to the widgets container.
- $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
+ $new_mode = $submit['button']['#paragraphs_mode'];
- $delta = array_slice($button['#array_parents'], -4, -3);
- $delta = $delta[0];
+ if ($new_mode === 'edit') {
+ $submit['widget_state'] = static::autocollapse($submit['widget_state']);
+ }
- $field_name = $element['#field_name'];
- $parents = $element['#field_parents'];
+ $submit['widget_state']['paragraphs'][$submit['delta']]['mode'] = $new_mode;
- $widget_state = static::getWidgetState($parents, $field_name, $form_state);
+ if (!empty($submit['button']['#paragraphs_show_warning'])) {
+ $submit['widget_state']['paragraphs'][$submit['delta']]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
+ }
+
+ static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
+
+ $form_state->setRebuild();
+ }
+
+ public static function itemAjax(array $form, FormStateInterface $form_state) {
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_ACTIONS);
- $widget_state['paragraphs'][$delta]['mode'] = $button['#paragraphs_mode'];
+ $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
+ $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
- if (!empty($button['#paragraphs_show_warning'])) {
- $widget_state['paragraphs'][$delta]['show_warning'] = $button['#paragraphs_show_warning'];
+ return $submit['element'];
+ }
+
+ /**
+ * Sets the form mode accordingly.
+ *
+ * @param array $form
+ * An associate array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public static function dragDropModeSubmit(array $form, FormStateInterface $form_state) {
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+
+ if (empty($submit['widget_state']['dragdrop'])) {
+ $submit['widget_state']['dragdrop'] = TRUE;
+ }
+ else {
+ $submit['widget_state']['dragdrop'] = FALSE;
}
- static::setWidgetState($parents, $field_name, $form_state, $widget_state);
+ // Make sure that flag that we already reordered is unset when the mode is
+ // switched.
+ unset($submit['widget_state']['reordered']);
+
+ // Switch the form mode accordingly.
+ static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
$form_state->setRebuild();
}
- public static function itemAjax(array $form, FormStateInterface $form_state) {
- $button = $form_state->getTriggeringElement();
- // Go one level up in the form, to the widgets container.
- $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
- $element['#prefix'] = '<div class="ajax-new-content">' . (isset($element['#prefix']) ? $element['#prefix'] : '');
- $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>';
+ /**
+ * Reorder paragraphs.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param $field_values_parents
+ * The field value parents.
+ */
+ protected static function reorderParagraphs(FormStateInterface $form_state, $field_values_parents) {
+ $field_name = end($field_values_parents);
+ $field_values = NestedArray::getValue($form_state->getValues(), $field_values_parents);
+ $complete_field_storage = NestedArray::getValue(
+ $form_state->getStorage(), [
+ 'field_storage',
+ '#parents'
+ ]
+ );
+ $new_field_storage = $complete_field_storage;
- return $element;
+ // Set a flag to prevent this from running twice, as the entity is built
+ // for validation as well as saving and would fail the second time as we
+ // already altered the field storage.
+ if (!empty($new_field_storage['#fields'][$field_name]['reordered'])) {
+ return;
+ }
+ $new_field_storage['#fields'][$field_name]['reordered'] = TRUE;
+
+ // Clear out all current paragraphs keys in all nested paragraph widgets
+ // as there might be fewer than before or none in a certain widget.
+ $clear_paragraphs = function ($field_storage) use (&$clear_paragraphs) {
+ foreach ($field_storage as $key => $value) {
+ if ($key === '#fields') {
+ foreach ($value as $field_name => $widget_state) {
+ if (isset($widget_state['paragraphs'])) {
+ $field_storage['#fields'][$field_name]['paragraphs'] = [];
+ }
+ }
+ }
+ else {
+ $field_storage[$key] = $clear_paragraphs($field_storage[$key]);
+ }
+ }
+ return $field_storage;
+ };
+
+ // Only clear the current field and its children to avoid deleting
+ // paragraph references in other fields.
+ $new_field_storage['#fields'][$field_name]['paragraphs'] = [];
+ if (isset($new_field_storage[$field_name])) {
+ $new_field_storage[$field_name] = $clear_paragraphs($new_field_storage[$field_name]);
+ }
+
+ $reorder_paragraphs = function ($reorder_values, $parents = [], FieldableEntityInterface $parent_entity = NULL) use ($complete_field_storage, &$new_field_storage, &$reorder_paragraphs) {
+ foreach ($reorder_values as $field_name => $values) {
+ foreach ($values['list'] as $delta => $item_values) {
+ $old_keys = array_merge(
+ $parents, [
+ '#fields',
+ $field_name,
+ 'paragraphs',
+ $delta
+ ]
+ );
+ $path = explode('][', $item_values['_path']);
+ $new_field_name = array_pop($path);
+ $key_parents = [];
+ foreach ($path as $i => $key) {
+ $key_parents[] = $key;
+ if ($i % 2 == 1) {
+ $key_parents[] = 'subform';
+ }
+ }
+ $new_keys = array_merge(
+ $key_parents, [
+ '#fields',
+ $new_field_name,
+ 'paragraphs',
+ $item_values['_weight']
+ ]
+ );
+ $key_exists = NULL;
+ $item_state = NestedArray::getValue($complete_field_storage, $old_keys, $key_exists);
+ if (!$key_exists && $parent_entity) {
+ // If key does not exist, then this parent widget was previously
+ // not expanded. This can only happen on nested levels. In that
+ // case, initialize a new item state and set the widget state to
+ // an empty array if it is not already set from an earlier item.
+ // If something else is placed there, it will be put in there,
+ // otherwise the widget will know that nothing is there anymore.
+ $item_state = [
+ 'entity' => $parent_entity->get($field_name)->get($delta)->entity,
+ 'mode' => 'closed',
+ ];
+ $widget_state_keys = array_slice($old_keys, 0, count($old_keys) - 2);
+ if (!NestedArray::getValue($new_field_storage, $widget_state_keys)) {
+ NestedArray::setValue($new_field_storage, $widget_state_keys, ['paragraphs' => []]);
+ }
+ }
+
+ // Ensure the referenced paragraph will be saved.
+ $item_state['entity']->setNeedsSave(TRUE);
+
+ NestedArray::setValue($new_field_storage, $new_keys, $item_state);
+ if (isset($item_values['dragdrop'])) {
+ $reorder_paragraphs(
+ $item_values['dragdrop'], array_merge(
+ $parents, [
+ $field_name,
+ $delta,
+ 'subform'
+ ]
+ ), $item_state['entity']
+ );
+ }
+ }
+ }
+ };
+ $reorder_paragraphs($field_values['dragdrop']);
+
+ // Recalculate original deltas.
+ $recalculate_original_deltas = function ($field_storage, ContentEntityInterface $parent_entity) use (&$recalculate_original_deltas) {
+ if (isset($field_storage['#fields'])) {
+ foreach ($field_storage['#fields'] as $field_name => $widget_state) {
+ if (isset($widget_state['paragraphs'])) {
+
+ // If the parent field does not exist but we have paragraphs in
+ // widget state, something went wrong and we have a mismatch.
+ // Throw an exception.
+ if (!$parent_entity->hasField($field_name) && !empty($widget_state['paragraphs'])) {
+ throw new \LogicException('Reordering paragraphs resulted in paragraphs on non-existing field ' . $field_name . ' on parent entity ' . $parent_entity->getEntityTypeId() . '/' . $parent_entity->id());
+ }
+
+ // Sort the paragraphs by key so that they will be assigned to
+ // the entity in the right order. Reset the deltas.
+ ksort($widget_state['paragraphs']);
+ $widget_state['paragraphs'] = array_values($widget_state['paragraphs']);
+
+ $original_deltas = range(0, count($widget_state['paragraphs']) - 1);
+ $field_storage['#fields'][$field_name]['original_deltas'] = $original_deltas;
+ $field_storage['#fields'][$field_name]['items_count'] = count($widget_state['paragraphs']);
+ $field_storage['#fields'][$field_name]['real_item_count'] = count($widget_state['paragraphs']);
+
+ // Update the parent entity and point to the new children, if the
+ // parent field does not exist, we also have no paragraphs, so
+ // we can just skip this, this is a dead leaf after re-ordering.
+ // @todo Clean this up somehow?
+ if ($parent_entity->hasField($field_name)) {
+ $parent_entity->set($field_name, array_column($widget_state['paragraphs'], 'entity'));
+
+ // Next process that field recursively.
+ foreach (array_keys($widget_state['paragraphs']) as $delta) {
+ if (isset($field_storage[$field_name][$delta]['subform'])) {
+ $field_storage[$field_name][$delta]['subform'] = $recalculate_original_deltas($field_storage[$field_name][$delta]['subform'], $parent_entity->get($field_name)->get($delta)->entity);
+ }
+ }
+ }
+
+ }
+ }
+ }
+ return $field_storage;
+ };
+
+ $parent_entity = $form_state->getFormObject()->getEntity();
+ $new_field_storage = $recalculate_original_deltas($new_field_storage, $parent_entity);
+
+ $form_state->set(['field_storage', '#parents'], $new_field_storage);
+ }
+
+ /**
+ * Ajax callback for the dragdrop mode.
+ *
+ * @param array $form
+ * An associate array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array
+ * The container form element.
+ */
+ public static function dragDropModeAjax(array $form, FormStateInterface $form_state) {
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+
+ $submit['element']['#prefix'] = '<div class="ajax-new-content">' . (isset($submit['element']['#prefix']) ? $submit['element']['#prefix'] : '');
+ $submit['element']['#suffix'] = (isset($submit['element']['#suffix']) ? $submit['element']['#suffix'] : '') . '</div>';
+
+ return $submit['element'];
}
/**
$delta = $element['#delta'];
if (isset($widget_state['paragraphs'][$delta]['entity'])) {
+ /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
$entity = $widget_state['paragraphs'][$delta]['entity'];
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
// Validate all enabled behavior plugins.
$paragraphs_type = $entity->getParagraphType();
- foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
- $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
- $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state);
+ if (\Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
+ foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin_values) {
+ $subform_state = SubformState::createForSubform($element['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
+ $plugin_values->validateBehaviorForm($entity, $element['behavior_plugins'][$plugin_id], $subform_state);
+ }
}
}
}
static::setWidgetState($element['#field_parents'], $field_name, $form_state, $widget_state);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
+ $field_name = $this->fieldDefinition->getName();
+
+ $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
+
+ // In dragdrop mode, validation errors can not be mapped to form elements,
+ // add them on the top level widget element.
+ if (!empty($field_state['dragdrop'])) {
+ if ($violations->count()) {
+ $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']);
+ foreach ($violations as $violation) {
+ $form_state->setError($element, $violation->getMessage());
+ }
+ }
+ }
+ else {
+ return parent::flagErrors($items, $violations, $form, $form_state);
+ }
+ }
+
+ /**
+ * Special handling to validate form elements with multiple values.
+ *
+ * @param array $elements
+ * An associative array containing the substructure of the form to be
+ * validated in this call.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $form
+ * The complete form array.
+ */
+ public function multipleElementValidate(array $elements, FormStateInterface $form_state, array $form) {
+ $field_name = $this->fieldDefinition->getName();
+ $widget_state = static::getWidgetState($elements['#field_parents'], $field_name, $form_state);
+
+ if ($elements['#required'] && $widget_state['real_item_count'] < 1) {
+ $form_state->setError($elements, t('@name field is required.', ['@name' => $this->fieldDefinition->getLabel()]));
+ }
+
+ static::setWidgetState($elements['#field_parents'], $field_name, $form_state, $widget_state);
+ }
+
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
- $entity = $form_state->getFormObject()->getEntity();
$field_name = $this->fieldDefinition->getName();
$widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
$element = NestedArray::getValue($form_state->getCompleteForm(), $widget_state['array_parents']);
- $new_revision = FALSE;
- if ($entity instanceof RevisionableInterface) {
- if ($entity->isNewRevision()) {
- $new_revision = TRUE;
- }
- // Most of the time we don't know yet if the host entity is going to be
- // saved as a new revision using RevisionableInterface::isNewRevision().
- // Most entity types (at least nodes) however use a boolean property named
- // "revision" to indicate whether a new revision should be saved. Use that
- // property.
- elseif ($entity->getEntityType()->hasKey('revision') && $form_state->getValue('revision')) {
- $new_revision = TRUE;
+ if (!empty($widget_state['dragdrop'])) {
+ $path = array_merge($form['#parents'], array($field_name));
+ static::reorderParagraphs($form_state, $path);
+
+ // After re-ordering, get the updated widget state.
+ $widget_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
+
+ // Re-create values based on current widget state.
+ $values = [];
+ foreach ($widget_state['paragraphs'] as $delta => $paragraph_state) {
+ $values[$delta]['entity'] = $paragraph_state['entity'];
}
+ return $values;
}
foreach ($values as $delta => &$item) {
if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
&& $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
+ /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
$paragraphs_entity = $widget_state['paragraphs'][$item['_original_delta']]['entity'];
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
- $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
+ $display = $widget_state['paragraphs'][$item['_original_delta']]['display'];
if ($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'edit') {
$display->extractFormValues($paragraphs_entity, $element[$item['_original_delta']]['subform'], $form_state);
}
- $paragraphs_entity->setNewRevision($new_revision);
// A content entity form saves without any rebuild. It needs to set the
// language to update it in case of language change.
$langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
if (!isset($item['behavior_plugins'][$plugin_id])) {
$item['behavior_plugins'][$plugin_id] = [];
}
- if (isset($element[$delta]) && isset($element[$delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm()) {
- $subform_state = SubformState::createForSubform($element[$delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
- $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state);
+ $original_delta = $item['_original_delta'];
+ if (isset($element[$original_delta]) && isset($element[$original_delta]['behavior_plugins'][$plugin_id]) && $form_state->getCompleteForm() && \Drupal::currentUser()->hasPermission('edit behavior plugin settings')) {
+ $subform_state = SubformState::createForSubform($element[$original_delta]['behavior_plugins'][$plugin_id], $form_state->getCompleteForm(), $form_state);
+ if (isset($item['behavior_plugins'][$plugin_id])) {
+ $plugin_values->submitBehaviorForm($paragraphs_entity, $item['behavior_plugins'][$plugin_id], $subform_state);
+ }
}
}
}
}
// If our mode is remove don't save or reference this entity.
// @todo: Maybe we should actually delete it here?
- elseif($widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') {
+ elseif (isset($widget_state['paragraphs'][$item['_original_delta']]['mode']) && $widget_state['paragraphs'][$item['_original_delta']]['mode'] == 'remove') {
$item['target_id'] = NULL;
$item['target_revision_id'] = NULL;
}
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
// Filter possible empty items.
$items->filterEmptyItems();
+
+ // Remove buttons from header actions.
+ $field_name = $this->fieldDefinition->getName();
+ $path = array_merge($form['#parents'], array($field_name));
+ $form_state_variables = $form_state->getValues();
+ $key_exists = NULL;
+ $values = NestedArray::getValue($form_state_variables, $path, $key_exists);
+
+ if ($key_exists) {
+ unset($values['header_actions']);
+
+ NestedArray::setValue($form_state_variables, $path, $values);
+ $form_state->setValues($form_state_variables);
+ }
+
return parent::extractFormValues($items, $form, $form_state);
}
* Initializes the translation form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
- * @param \Drupal\Core\Entity\EntityInterface $host
+ * @param \Drupal\Core\Entity\ContentEntityInterface $host
*/
- protected function initIsTranslating(FormStateInterface $form_state, EntityInterface $host) {
+ protected function initIsTranslating(FormStateInterface $form_state, ContentEntityInterface $host) {
if ($this->isTranslating != NULL) {
return;
}
/**
* Returns the default paragraph type.
*
- * @return string $default_paragraph_type
+ * @return string
* Label name for default paragraph type.
*/
- protected function getDefaultParagraphTypeLabelName(){
+ protected function getDefaultParagraphTypeLabelName() {
if ($this->getDefaultParagraphTypeMachineName() !== NULL) {
$allowed_types = $this->getAllowedTypes();
return $allowed_types[$this->getDefaultParagraphTypeMachineName()]['label'];
}
/**
- * @param \Drupal\paragraphs\Entity\Paragraph $paragraphs_entity
- * Entity where to extract the values.
+ * Counts the number of paragraphs in a certain mode in a form substructure.
+ *
+ * @param array $widget_state
+ * The widget state for the form substructure containing information about
+ * the paragraphs within.
+ * @param string $mode
+ * The mode to look for.
*
- * @return string $collapsed_summary_text
- * The text without tags to return.
+ * @return int
+ * The number of paragraphs is the given mode.
*/
- public function addCollapsedSummary(paragraphs\Entity\Paragraph $paragraphs_entity) {
- $text_types = ['text_with_summary', 'text', 'text_long', 'list_string'];
- $summary = [];
- foreach ($paragraphs_entity->getFieldDefinitions() as $key => $value) {
- if ($value->getType() == 'image') {
- if ($paragraphs_entity->get($key)->entity) {
- foreach ($paragraphs_entity->get($key) as $image_key => $image_value) {
- if ($image_value->title != '') {
- $text = $image_value->title;
- }
- elseif ($image_value->alt != '') {
- $text = $image_value->alt;
- }
- elseif ($text = $image_value->entity->filename->value) {
- $text = $image_value->entity->filename->value;
- }
- if (strlen($text) > 50) {
- $text = strip_tags(substr($text, 0, 150));
- }
- $summary[] = $text;
- }
- }
- }
- if (in_array($value->getType(), $text_types)) {
- $text = $paragraphs_entity->get($key)->value;
- if (strlen($text) > 50) {
- $text = strip_tags(substr($text, 0, 150));
- }
- $summary[] = $text;
- }
- if ($field_type = $value->getType() == 'entity_reference_revisions') {
- if ($paragraphs_entity->get($key) && $paragraphs_entity->get($key)->entity) {
- $summary[] = $this->addCollapsedSummary($paragraphs_entity->get($key)->entity);
- }
- }
- if ($field_type = $value->getType() == 'entity_reference') {
- if (!in_array($key, ['type', 'uid', 'revision_uid'])) {
- if ($paragraphs_entity->get($key)->entity) {
- $summary[] = $paragraphs_entity->get($key)->entity->label();
- }
- }
- }
+ protected function getNumberOfParagraphsInMode(array $widget_state, $mode) {
+ if (!isset($widget_state['paragraphs'])) {
+ return 0;
}
- $paragraphs_type = $paragraphs_entity->getParagraphType();
- foreach ($paragraphs_type->getEnabledBehaviorPlugins() as $plugin_id => $plugin) {
- if ($plugin_summary = $plugin->settingsSummary($paragraphs_entity)) {
- $summary = array_merge($summary, $plugin_summary);
+
+ $paragraphs_count = 0;
+ foreach ($widget_state['paragraphs'] as $paragraph) {
+ if ($paragraph['mode'] == $mode) {
+ $paragraphs_count++;
}
}
- $collapsed_summary_text = implode(', ', $summary);
- return strip_tags($collapsed_summary_text);
+
+ return $paragraphs_count;
}
/**
$target_type = $field_definition->getSetting('target_type');
$paragraph_type = \Drupal::entityTypeManager()->getDefinition($target_type);
if ($paragraph_type) {
- return $paragraph_type->isSubclassOf(ParagraphInterface::class);
+ return $paragraph_type->entityClassImplements(ParagraphInterface::class);
}
return FALSE;
}
+ /**
+ * Builds header actions.
+ *
+ * @param array[] $field_state
+ * Field widget state.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Current form state.
+ *
+ * @return array[]
+ * The form element array.
+ */
+ public function buildHeaderActions(array $field_state, FormStateInterface $form_state) {
+ $actions = [];
+ if (empty($this->fieldParents)) {
+ // Set actions.
+ $actions = [
+ '#type' => 'paragraphs_actions',
+ ];
+
+ $field_name = $this->fieldDefinition->getName();
+ $id_prefix = implode('-', array_merge($this->fieldParents, [$field_name]));
+
+ // Only show the dragdrop mode if we can find the sortable library.
+ $library_discovery = \Drupal::service('library.discovery');
+ $library = $library_discovery->getLibraryByName('paragraphs', 'paragraphs-dragdrop');
+ if ($library || \Drupal::state()->get('paragraphs_test_dragdrop_force_show', FALSE)) {
+ $dragdrop_mode = $this->expandButton([
+ '#type' => 'submit',
+ '#name' => $this->fieldIdPrefix . '_dragdrop_mode',
+ '#value' => !empty($field_state['dragdrop']) ? $this->t('Complete drag & drop') : $this->t('Drag & drop'),
+ '#attributes' => ['class' => ['field-dragdrop-mode-submit']],
+ '#submit' => [[get_class($this), 'dragDropModeSubmit']],
+ '#weight' => 8,
+ '#ajax' => [
+ 'callback' => [get_class($this), 'dragDropModeAjax'],
+ 'wrapper' => $this->fieldWrapperId,
+ ],
+ ]);
+
+ // Make the complete button a primary button, limit validation errors
+ // only for enabling drag and drop mode.
+ if (!empty($field_state['dragdrop'])) {
+ $dragdrop_mode['#button_type'] = 'primary';
+ $actions['actions']['dragdrop_mode'] = $dragdrop_mode;
+ }
+ else {
+ $dragdrop_mode['#limit_validation_errors'] = [
+ array_merge($this->fieldParents, [$field_name, 'dragdrop_mode']),
+ ];
+ $actions['dropdown_actions']['dragdrop_mode'] = $dragdrop_mode;
+ }
+ }
+
+ if ($this->realItemCount > 1 && empty($field_state['dragdrop'])) {
+
+ $collapse_all = $this->expandButton([
+ '#type' => 'submit',
+ '#value' => $this->t('Collapse all'),
+ '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
+ '#name' => $id_prefix . '_collapse_all',
+ '#paragraphs_mode' => 'closed',
+ '#limit_validation_errors' => [
+ array_merge($this->fieldParents, [$field_name, 'collapse_all']),
+ ],
+ '#ajax' => [
+ 'callback' => [get_class($this), 'allActionsAjax'],
+ 'wrapper' => $this->fieldWrapperId,
+ ],
+ '#weight' => -1,
+ '#paragraphs_show_warning' => TRUE,
+ ]);
+
+ $edit_all = $this->expandButton([
+ '#type' => 'submit',
+ '#value' => $this->t('Edit all'),
+ '#submit' => [[get_class($this), 'changeAllEditModeSubmit']],
+ '#name' => $id_prefix . '_edit-all',
+ '#paragraphs_mode' => 'edit',
+ '#limit_validation_errors' => [],
+ '#ajax' => [
+ 'callback' => [get_class($this), 'allActionsAjax'],
+ 'wrapper' => $this->fieldWrapperId,
+ ],
+ ]);
+
+ if (isset($field_state['paragraphs'][0]['mode']) && $field_state['paragraphs'][0]['mode'] === 'closed') {
+ $edit_all['#attributes'] = [
+ 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-edit'],
+ 'title' => $this->t('Edit all'),
+ ];
+ $edit_all['#title'] = $this->t('Edit All');
+ $actions['actions']['edit_all'] = $edit_all;
+ $actions['dropdown_actions']['collapse_all'] = $collapse_all;
+ }
+ else {
+ $collapse_all['#attributes'] = [
+ 'class' => ['paragraphs-icon-button', 'paragraphs-icon-button-collapse'],
+ 'title' => $this->t('Collapse all'),
+ ];
+ $actions['actions']['collapse_all'] = $collapse_all;
+ $actions['dropdown_actions']['edit_all'] = $edit_all;
+ }
+ }
+ }
+
+ // Add paragraphs_header flag which we use later in preprocessor to move
+ // header actions to table header.
+ if ($actions) {
+ $actions['#paragraphs_header'] = TRUE;
+ }
+
+ return $actions;
+ }
+
+ /**
+ * Loops through all paragraphs and change mode for each paragraph instance.
+ *
+ * @param array $form
+ * Current form state.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Current form state.
+ */
+ public static function changeAllEditModeSubmit(array $form, FormStateInterface $form_state) {
+ $submit = ParagraphsWidget::getSubmitElementInfo($form, $form_state, ParagraphsWidget::ACTION_POSITION_HEADER);
+
+ // Change edit mode for each paragraph.
+ foreach ($submit['widget_state']['paragraphs'] as $delta => &$paragraph) {
+ if ($submit['widget_state']['paragraphs'][$delta]['mode'] !== 'remove') {
+ $submit['widget_state']['paragraphs'][$delta]['mode'] = $submit['button']['#paragraphs_mode'];
+ if (!empty($submit['button']['#paragraphs_show_warning'])) {
+ $submit['widget_state']['paragraphs'][$delta]['show_warning'] = $submit['button']['#paragraphs_show_warning'];
+ }
+ }
+ }
+
+ // Disable autocollapse when editing all and enable it when closing all.
+ if ($submit['button']['#paragraphs_mode'] === 'edit') {
+ $submit['widget_state']['autocollapse'] = 'none';
+ }
+ elseif ($submit['button']['#paragraphs_mode'] === 'closed') {
+ $submit['widget_state']['autocollapse'] = 'all';
+ }
+
+ static::setWidgetState($submit['parents'], $submit['field_name'], $form_state, $submit['widget_state']);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Returns a state with all paragraphs closed, if autocollapse is enabled.
+ *
+ * @param array $widget_state
+ * The current widget state.
+ *
+ * @return array
+ * The widget state altered by closing all paragraphs.
+ */
+ public static function autocollapse(array $widget_state) {
+ if ($widget_state['real_item_count'] > 0 && $widget_state['autocollapse'] !== 'none') {
+ foreach ($widget_state['paragraphs'] as $delta => $value) {
+ if ($widget_state['paragraphs'][$delta]['mode'] === 'edit') {
+ $widget_state['paragraphs'][$delta]['mode'] = 'closed';
+ }
+ }
+ }
+
+ return $widget_state;
+ }
+
}