diff --git a/css/paragraphs.admin.css b/css/paragraphs.admin.css index 58971cc..eba31ec 100644 --- a/css/paragraphs.admin.css +++ b/css/paragraphs.admin.css @@ -80,3 +80,35 @@ padding-right: 10px; text-align: right; } + + +.js .paragraph-type-add-modal { + width: 100%; + padding: 10px 0; + text-align: center; + margin-bottom: -3em; + height: 30px; +} + +.js .paragraph-type-add-modal.first-button { + margin-top: -32px; + margin-bottom: -20px; +} + +.js .clearfix > .paragraph-type-add-modal.first-button { + margin-left: 12px; +} + +.js .paragraph-type-add-modal.no-paragraphs { + margin-bottom: -1em; +} + +.js .paragraph-type-add-modal-button { + display: inline-block; + margin: 0 auto; +} + +.js .paragraph-type-add-modal-button:hover { + color: #ffffff; + background: #057ec7 none; +} diff --git a/css/paragraphs.modal.css b/css/paragraphs.modal.css new file mode 100644 index 0000000..01aa393 --- /dev/null +++ b/css/paragraphs.modal.css @@ -0,0 +1,12 @@ +ul.paragraphs-add-dialog-list { + margin: 0; + list-style-type: none; +} + +.paragraphs-add-dialog-row { + padding: 5px; +} + +.paragraphs-add-dialog-row button { + width: 200px; +} diff --git a/css/paragraphs.widget.css b/css/paragraphs.widget.css index 9994da3..29d7322 100644 --- a/css/paragraphs.widget.css +++ b/css/paragraphs.widget.css @@ -80,6 +80,37 @@ text-align: right; } +.js .paragraph-type-add-modal { + width: 100%; + padding: 10px 0; + text-align: center; + margin-bottom: -3em; + height: 30px; +} + +.js .paragraph-type-add-modal.first-button { + margin-top: -32px; + margin-bottom: -20px; +} + +.js .clearfix > .paragraph-type-add-modal.first-button { + margin-left: 12px; +} + +.js .paragraph-type-add-modal.no-paragraphs { + margin-bottom: -1em; +} + +.js .paragraph-type-add-modal-button { + display: inline-block; + margin: 0 auto; +} + +.js .paragraph-type-add-modal-button:hover { + color: #ffffff; + background: #057ec7 none; +} + .paragraph--view-mode--preview { padding-right: 1em; } diff --git a/js/paragraphs.modal.js b/js/paragraphs.modal.js new file mode 100644 index 0000000..2670d4d --- /dev/null +++ b/js/paragraphs.modal.js @@ -0,0 +1,257 @@ +/** + * @file paragraphs.modal.js + * + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Method to apply data on template element. + * + * For more information about template please take a look at documentation + * provided in: templates/paragraphs-add-dialog.html.twig + * + * @param {Object} $templateElement + * jQuery element for template element. + * @param {Object} data + * Data object used to apply on template. + * + * @private + */ + var _templateApplyData = function ($templateElement, data) { + $.each(data, function (name, value) { + var selector = '[data-' + name + ']'; + + $templateElement.find(selector) + .addBack(selector) + .each(function (index, childElement) { + var $child = $(childElement); + var attrName = $child.attr('data-' + name); + $child.removeAttr('data-' + name); + + if (attrName === 'content') { + $child.text(value); + } + else { + $child.attr(attrName, value); + } + }); + }); + }; + + /** + * Method to clone template and apply data on it. + * + * @param {Object} $template + * jQuery element for template element used for cloning. + * @param {Object} data + * Data object used to apply on template. + * + * @return {Object} + * Returns cloned template with applied data on it. + * + * @private + */ + var _templateClone = function ($template, data) { + var $templateClone = $template + .clone() + .appendTo($template.parent()); + + // Adjust all CSS classes with suffix "-template". + var classEnding = '-template'; + $.each( + $templateClone.attr('class').toString().split(' '), + function (index, className) { + if (className.substr(-classEnding.length) === classEnding) { + var newClassName = className.substr(0, className.length - classEnding.length); + $templateClone + .removeClass(className) + .toggleClass(newClassName); + } + } + ); + + _templateApplyData($templateClone, data); + + return $templateClone; + }; + + /** + * Click handler for click "+ Add" button between paragraphs. + * + * @type {Object} + */ + Drupal.behaviors.modalAdd = { + attach: function (context) { + $('.paragraph-type-add-modal-button', context) + .once('add-click-handler') + .on('click', function (event) { + var $button = $(this); + + // Stop default execution of click event. + event.preventDefault(); + event.stopPropagation(); + + // Global data for dialog template. + // delta: '' -> means that paragraph will be added to end of list. + var config = {delta: ''}; + + // Get wrapper of elements used for submitting of add paragraph + // functionality. It's also used as context for opening of dialog. + var $addMoreWrapper; + if ($button.attr('name') === 'first_button_add_modal') { + // Case for last button in list (of for empty list). + $addMoreWrapper = $button + .parent() + .siblings('.js-hide') + .parent(); + } + else { + // For button between paragraphs. + $addMoreWrapper = $button + .closest('table') + .parent() + .find('.paragraphs-add-dialog-template') + .parent(); + + // Set delta to correct position in list of paragraphs. + config.delta = $button.closest('tr').index(); + } + + // Get types from ComboBox. + var paragraphTypes = Drupal.modalAddParagraphs.getParagraphTypes($addMoreWrapper.find('[name$="[add_more_select]"]')); + + // Open dialog in context of "add_more" element, with provided + // configuration and list of paragraph types. + Drupal.modalAddParagraphs.openDialog($addMoreWrapper, config, paragraphTypes); + }); + } + }; + + /** + * Namespace for modal related javascript methods. + * + * @type {Object} + */ + Drupal.modalAddParagraphs = {}; + + /** + * Fetch list of paragraph types from options provided in combobox. + * + * @param {Object} $typeComboBox + * jQuery element of combobox with list of available paragraph types. + * + * @return {Array} + * List of paragraph type objects with type and it's name. + */ + Drupal.modalAddParagraphs.getParagraphTypes = function ($typeComboBox) { + var paragraphTypes = []; + + $typeComboBox.find('option').each(function (index, optionElement) { + var $option = $(optionElement); + + paragraphTypes.push( + { + 'type': $option.attr('value'), + 'type-name': $option.text() + } + ); + }); + + return paragraphTypes; + }; + + /** + * Open modal dialog for adding new paragraph in list. + * + * @param {Object} $context + * jQuery element of form wrapper used to submit request for adding new + * paragraph to list. Wrapper also contains dialog template. + * @param {Object} config + * Global configuration for dialog template. It will be applied on dialog + * template to generated customized dialog. + * @param {Array} paragraphTypes + * List of objects with information for paragraph type. Every object + * contains type and name for it and list is used to generate correct action + * elements in customized dialog. + */ + Drupal.modalAddParagraphs.openDialog = function ($context, config, paragraphTypes) { + + // Get dialog template and apply data on it. + var $dialogTemplate = $('.paragraphs-add-dialog-template', $context); + var $dialog = _templateClone($dialogTemplate, config); + + // Get row template and duplicate it for every paragraph type. + var $rowTemplate = $dialog.find('.paragraphs-add-dialog-row-template'); + for (var i = 0; i < paragraphTypes.length; i++) { + _templateClone($rowTemplate, paragraphTypes[i]); + } + $rowTemplate.remove(); + + // Open dialog after data is applied on template. + $dialog.dialog({ + modal: true, + resizable: false, + close: function () { + var $dialog = $(this); + + // Destroy dialog object. + $dialog.dialog('destroy'); + + // Remove created html element for dialog. + $dialog.remove(); + } + }); + + // Attach behaviours to dialog action triggering elements. + $('.paragraphs-add-type-trigger-element', $dialog) + .once('add-click-handler') + .on('click', function (event) { + var $this = $(this); + + // Stop default execution of click event. + event.preventDefault(); + event.stopPropagation(); + + Drupal.modalAddParagraphs.setValues( + $context, + { + add_more_select: $this.attr('data-type'), + add_more_delta: $this.attr('data-delta') + } + ); + + // Close dialog afterwards. + $this.closest('div.ui-dialog-content').dialog('close'); + }); + }; + + /** + * Method to set hidden fields and trigger adding of paragraph. + * + * @param {Object} $context + * Jquery object containing element where form for submitting exists. + * @param {Object} options + * Object with key value pair, where key is name of field that should be set + * and value is value for that field. + */ + Drupal.modalAddParagraphs.setValues = function ($context, options) { + var $submitButton = $('.js-hide input[name$="_add_more"]', $context); + var $wrapper = $submitButton.parent(); + + // Set all field values defined in options. + $.each(options, function (fieldName, fieldValue) { + var $field = $wrapper.find('[name$="[' + fieldName + ']"]'); + + if ($field) { + $field.val(fieldValue); + } + }); + + // Trigger ajax call on add button. + $submitButton.trigger('mousedown'); + }; + +}(jQuery, Drupal, drupalSettings)); diff --git a/paragraphs.libraries.yml b/paragraphs.libraries.yml index bb2015d..00ab60f 100644 --- a/paragraphs.libraries.yml +++ b/paragraphs.libraries.yml @@ -16,3 +16,15 @@ drupal.paragraphs.widget: css: theme: css/paragraphs.widget.css: {} + +drupal.paragraphs.modal: + js: + js/paragraphs.modal.js: {} + css: + theme: + css/paragraphs.modal.css: {} + dependencies: + - core/drupal.dialog.ajax + - core/jquery.once + - core/jquery.ui.tabs + - core/drupal diff --git a/paragraphs.module b/paragraphs.module index 7d73bfd..c05a6b6 100644 --- a/paragraphs.module +++ b/paragraphs.module @@ -59,6 +59,11 @@ function paragraphs_theme() { 'paragraphs_dropbutton_wrapper' => array( 'variables' => array('children' => NULL), ), + 'paragraphs_add_dialog' => [ + 'variables' => [], + 'template' => 'paragraphs-add-dialog', +// 'render element' => 'elements', + ], ); } diff --git a/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php index 4a6b0bf..aa33320 100644 --- a/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php +++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php @@ -135,7 +135,8 @@ class InlineParagraphsWidget extends WidgetBase { '#options' => array( 'select' => $this->t('Select list'), 'button' => $this->t('Buttons'), - 'dropdown' => $this->t('Dropdown button') + 'dropdown' => $this->t('Dropdown button'), + 'modal' => $this->t('Modal form'), ), '#default_value' => $this->getSetting('add_mode'), '#required' => TRUE, @@ -201,6 +202,9 @@ class InlineParagraphsWidget extends WidgetBase { case 'dropdown': $add_mode = $this->t('Dropdown button'); break; + case 'modal': + $add_mode = $this->t('Modal form'); + break; } $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]); @@ -260,17 +264,6 @@ class InlineParagraphsWidget extends WidgetBase { } } } - elseif (isset($widget_state['selected_bundle'])) { - - $entity_type = $entity_manager->getDefinition($target_type); - $bundle_key = $entity_type->getKey('bundle'); - - $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array( - $bundle_key => $widget_state['selected_bundle'], - )); - - $item_mode = 'edit'; - } if ($item_mode == 'collapsed') { $item_mode = $default_edit_mode; @@ -689,6 +682,10 @@ class InlineParagraphsWidget extends WidgetBase { $widget_state['paragraphs'][$delta]['display'] = $display; $widget_state['paragraphs'][$delta]['mode'] = $item_mode; + if ($this->getSetting('add_mode') == 'modal') { + $this->buildInBetweenAddButton($element, $id_prefix); + } + static::setWidgetState($parents, $field_name, $form_state, $widget_state); } else { @@ -698,6 +695,47 @@ class InlineParagraphsWidget extends WidgetBase { return $element; } + /** + * Builds an add paragraph button between paragraphs for opening of modal. + * + * @param array $element + * Render element. + * @param string $id_prefix + * Prefix. + */ + protected function buildInBetweenAddButton(array &$element, $id_prefix) { + if (empty($this->uuid)) { + $this->uuid = \Drupal::service('uuid')->generate(); + } + + $element[$id_prefix . '_area'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'paragraph-type-add-modal', + 'first-button', + ], + ], + '#access' => !$this->isTranslating, + '#weight' => -2000, + ]; + + $element[$id_prefix . '_area']['add_more'] = [ + '#type' => 'submit', + '#value' => $this->t('+ Add'), + '#name' => strtr($id_prefix, '-', '_') . '_add_modal', + '#attributes' => [ + 'class' => [ + 'paragraph-type-add-modal-button', + 'button--small', + 'js-show', + ], + ], + ]; + + $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.modal'; + } + public function getAllowedTypes() { $return_bundles = array(); @@ -848,6 +886,7 @@ class InlineParagraphsWidget extends WidgetBase { $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); if ($this->realItemCount > 0) { @@ -945,7 +984,7 @@ class InlineParagraphsWidget extends WidgetBase { ]; } - return $add_more_elements ; + return $add_more_elements; } if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') { @@ -1048,7 +1087,24 @@ class InlineParagraphsWidget extends WidgetBase { protected function buildSelectAddMode() { $field_name = $this->fieldDefinition->getName(); $title = $this->fieldDefinition->getLabel(); - $add_more_elements['add_more_select'] = [ + + $add_more_elements = []; + + // Add button at end of list, when modal dialog is used to add paragraphs. + if ($this->getSetting('add_mode') == 'modal') { + $this->buildInBetweenAddButton($add_more_elements, 'first-button'); + + // Append template for modal add paragraph dialog. + $add_more_elements['paragraphs_add_dialog_template'] = $this->getModalAddDialogTemplate(); + + // Selection form elements. + $selection_form_elements['add_more_delta'] = [ + '#type' => 'hidden', + '#title' => 'Delta', + ]; + } + + $selection_form_elements['add_more_select'] = [ '#type' => 'select', '#options' => $this->getAccessibleOptions(), '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]), @@ -1056,12 +1112,11 @@ class InlineParagraphsWidget extends WidgetBase { ]; $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]); - if ($this->realItemCount > 0) { $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]); } - $add_more_elements['add_more_button'] = [ + $selection_form_elements['add_more_button'] = [ '#type' => 'submit', '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more', '#value' => $text, @@ -1074,8 +1129,18 @@ class InlineParagraphsWidget extends WidgetBase { 'effect' => 'fade', ], ]; + $selection_form_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]); + + // If we have a modal, the selection form should be hidden. + if ($this->getSetting('add_mode') == 'modal') { + $add_more_elements['selection_form'] = $selection_form_elements; + $add_more_elements['selection_form']['#type'] = 'container'; + $add_more_elements['selection_form']['#attributes']['class'][] = 'js-hide'; + } + else { + $add_more_elements = $selection_form_elements; + } - $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]); return $add_more_elements; } @@ -1101,6 +1166,10 @@ class InlineParagraphsWidget extends WidgetBase { $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)); + // The structure of modal is different. + if (!isset($element['#field_name'])) { + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3)); + } // Add a DIV around the delta receiving the Ajax effect. $delta = $element['#max_delta']; @@ -1118,6 +1187,11 @@ class InlineParagraphsWidget extends WidgetBase { // Go one level up in the form, to the widgets container. $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2)); + // The structure of modal is different. + if (!isset($element['#field_name'])) { + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3)); + } + $field_name = $element['#field_name']; $parents = $element['#field_parents']; @@ -1126,20 +1200,104 @@ class InlineParagraphsWidget extends WidgetBase { if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { $widget_state['items_count']++; + $widget_state['real_item_count']++; } + $position = $widget_state['real_item_count'] - 1; if (isset($button['#bundle_machine_name'])) { $widget_state['selected_bundle'] = $button['#bundle_machine_name']; } + elseif ($widget_state['add_mode'] == 'modal') { + $widget_state['selected_bundle'] = $element['add_more']['selection_form']['add_more_select']['#value']; + if ($element['add_more']['selection_form']['add_more_delta']['#value'] !== "") { + $position = $element['add_more']['selection_form']['add_more_delta']['#value']; + } + } else { $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value']; } + // Create new paragraph entity. + $entity_type = \Drupal::entityTypeManager()->getDefinition('paragraph'); + $bundle_key = $entity_type->getKey('bundle'); + + $paragraphs_entity = \Drupal::entityTypeManager()->getStorage('paragraph')->create(array( + $bundle_key => $widget_state['selected_bundle'], + )); + + $paragraph = [ + 'entity' => $paragraphs_entity, + 'mode' => 'edit', + ]; + + if (isset($widget_state['paragraphs'][$position])) { + $paragraph['display'] = $widget_state['paragraphs'][$position]['display']; + } + + // Add paragraph to the widget state. + // This also creates it, when it doesn't exist. + $widget_state['paragraphs'][] = $paragraph; + + // If we use a modal, we have to place the new paragraph + // in the correct position. + if ($widget_state['add_mode'] == 'modal') { + // Clean form_state. + $user_input = $user_input_save = $form_state->getUserInput(); + + // Temporarily remove 'add_more' from array. + unset($user_input[$field_name]['add_more']); + + self::normalizeWeights($user_input[$field_name], $position); + + // It is important, that the key maps with the one from $widget_state + // The weight handles the position. + $key = count($widget_state['paragraphs']) - 1; + $item[$key] = [ + 'subform' => [], + '_weight' => $position, + ]; + + $user_input[$field_name] += $item; + + // Add back 'add_more'. + $user_input[$field_name]['add_more'] = $user_input_save[$field_name]; + + $form_state->setUserInput($user_input); + } + static::setWidgetState($parents, $field_name, $form_state, $widget_state); $form_state->setRebuild(); } + /** + * This resets the weights, ascending, starting from 0. + * + * It keeps the indices. + * + * E.g. [-6, -5, -4] will become [0, 1, 2] + * + * Starting at position, the elements will get weight+1, to make space + * for a new element. + * + * E.g. [-6, -5, -4], position = 1, will become [ 0, 2, 3] + * + * @param array $array + * The array, which weights should be normalised. + * @param int $position + * The position from which on the weights will be increased by 1. + */ + private static function normalizeWeights(array &$array, $position) { + $weigth = 0; + foreach ($array as &$value) { + if ($weigth == $position) { + $weigth++; + } + $value['_weight'] = $weigth; + $weigth++; + } + } + public static function paragraphsItemSubmit(array $form, FormStateInterface $form_state) { $button = $form_state->getTriggeringElement(); @@ -1460,6 +1618,21 @@ class InlineParagraphsWidget extends WidgetBase { return strip_tags($collapsed_summary_text); } + /** + * Returns render array for template of add paragraph modal dialog. + * + * @return array + * Render array. + */ + protected function getModalAddDialogTemplate() { + return [ + '#theme' => 'paragraphs_add_dialog', + '#attached' => [ + 'library' => ['paragraphs/drupal.paragraphs.modal'], + ], + ]; + } + /** * {@inheritdoc} */ diff --git a/src/Plugin/Field/FieldWidget/ParagraphsWidget.php b/src/Plugin/Field/FieldWidget/ParagraphsWidget.php index eda8415..4669fcb 100644 --- a/src/Plugin/Field/FieldWidget/ParagraphsWidget.php +++ b/src/Plugin/Field/FieldWidget/ParagraphsWidget.php @@ -130,7 +130,8 @@ class ParagraphsWidget extends WidgetBase { '#options' => array( 'select' => $this->t('Select list'), 'button' => $this->t('Buttons'), - 'dropdown' => $this->t('Dropdown button') + 'dropdown' => $this->t('Dropdown button'), + 'modal' => $this->t('Modal form'), ), '#default_value' => $this->getSetting('add_mode'), '#required' => TRUE, @@ -196,6 +197,9 @@ class ParagraphsWidget extends WidgetBase { case 'dropdown': $add_mode = $this->t('Dropdown button'); break; + case 'modal': + $add_mode = $this->t('Modal form'); + break; } $summary[] = $this->t('Edit mode: @edit_mode', ['@edit_mode' => $edit_mode]); @@ -227,7 +231,6 @@ class ParagraphsWidget extends WidgetBase { $host = $items->getEntity(); $widget_state = static::getWidgetState($parents, $field_name, $form_state); - $entity_manager = \Drupal::entityTypeManager(); $target_type = $this->getFieldSetting('target_type'); $item_mode = isset($widget_state['paragraphs'][$delta]['mode']) ? $widget_state['paragraphs'][$delta]['mode'] : 'edit'; @@ -255,17 +258,6 @@ class ParagraphsWidget extends WidgetBase { } } } - elseif (isset($widget_state['selected_bundle'])) { - - $entity_type = $entity_manager->getDefinition($target_type); - $bundle_key = $entity_type->getKey('bundle'); - - $paragraphs_entity = $entity_manager->getStorage($target_type)->create(array( - $bundle_key => $widget_state['selected_bundle'], - )); - - $item_mode = 'edit'; - } if ($item_mode == 'closed') { // Validate closed paragraphs and expand if needed. @@ -676,6 +668,10 @@ class ParagraphsWidget extends WidgetBase { $widget_state['paragraphs'][$delta]['display'] = $display; $widget_state['paragraphs'][$delta]['mode'] = $item_mode; + if ($this->getSetting('add_mode') == 'modal') { + $this->buildInBetweenAddButton($element, $id_prefix); + } + static::setWidgetState($parents, $field_name, $form_state, $widget_state); } else { @@ -685,6 +681,47 @@ class ParagraphsWidget extends WidgetBase { return $element; } + /** + * Builds an add paragraph button between paragraphs for opening of modal. + * + * @param array $element + * Render element. + * @param string $id_prefix + * Prefix. + */ + protected function buildInBetweenAddButton(array &$element, $id_prefix) { + if (empty($this->uuid)) { + $this->uuid = \Drupal::service('uuid')->generate(); + } + + $element[$id_prefix . '_area'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'paragraph-type-add-modal', + 'first-button', + ], + ], + '#access' => !$this->isTranslating, + '#weight' => -2000, + ]; + + $element[$id_prefix . '_area']['add_more'] = [ + '#type' => 'submit', + '#value' => $this->t('+ Add'), + '#name' => strtr($id_prefix, '-', '_') . '_add_modal', + '#attributes' => [ + 'class' => [ + 'paragraph-type-add-modal-button', + 'button--small', + 'js-show', + ], + ], + ]; + + $element['#attached']['library'][] = 'paragraphs/drupal.paragraphs.modal'; + } + public function getAllowedTypes() { $return_bundles = array(); @@ -838,6 +875,7 @@ class ParagraphsWidget extends WidgetBase { $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); if ($this->realItemCount > 0) { @@ -935,7 +973,7 @@ class ParagraphsWidget extends WidgetBase { ]; } - return $add_more_elements ; + return $add_more_elements; } if ($this->getSetting('add_mode') == 'button' || $this->getSetting('add_mode') == 'dropdown') { @@ -1038,7 +1076,24 @@ class ParagraphsWidget extends WidgetBase { protected function buildSelectAddMode() { $field_name = $this->fieldDefinition->getName(); $title = $this->fieldDefinition->getLabel(); - $add_more_elements['add_more_select'] = [ + + $add_more_elements = []; + + // Add button at end of list, when modal dialog is used to add paragraphs. + if ($this->getSetting('add_mode') == 'modal') { + $this->buildInBetweenAddButton($add_more_elements, 'first-button'); + + // Append template for modal add paragraph dialog. + $add_more_elements['paragraphs_add_dialog_template'] = $this->getModalAddDialogTemplate(); + + // Selection form elements. + $selection_form_elements['add_more_delta'] = [ + '#type' => 'hidden', + '#title' => 'Delta', + ]; + } + + $selection_form_elements['add_more_select'] = [ '#type' => 'select', '#options' => $this->getAccessibleOptions(), '#title' => $this->t('@title type', ['@title' => $this->getSetting('title')]), @@ -1046,12 +1101,11 @@ class ParagraphsWidget extends WidgetBase { ]; $text = $this->t('Add @title', ['@title' => $this->getSetting('title')]); - if ($this->realItemCount > 0) { $text = $this->t('Add another @title', ['@title' => $this->getSetting('title')]); } - $add_more_elements['add_more_button'] = [ + $selection_form_elements['add_more_button'] = [ '#type' => 'submit', '#name' => strtr($this->fieldIdPrefix, '-', '_') . '_add_more', '#value' => $text, @@ -1064,8 +1118,19 @@ class ParagraphsWidget extends WidgetBase { 'effect' => 'fade', ], ]; + $selection_form_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]); + + + // If we have a modal, the selection form should be hidden. + if ($this->getSetting('add_mode') == 'modal') { + $add_more_elements['selection_form'] = $selection_form_elements; + $add_more_elements['selection_form']['#type'] = 'container'; + $add_more_elements['selection_form']['#attributes']['class'][] = 'js-hide'; + } + else { + $add_more_elements = $selection_form_elements; + } - $add_more_elements['add_more_button']['#suffix'] = $this->t(' to %type', ['%type' => $title]); return $add_more_elements; } @@ -1076,6 +1141,10 @@ class ParagraphsWidget extends WidgetBase { $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)); + // The structure of modal is different. + if (!isset($element['#field_name'])) { + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3)); + } // Add a DIV around the delta receiving the Ajax effect. $delta = $element['#max_delta']; @@ -1093,6 +1162,11 @@ class ParagraphsWidget extends WidgetBase { // Go one level up in the form, to the widgets container. $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2)); + // The structure of modal is different. + if (!isset($element['#field_name'])) { + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3)); + } + $field_name = $element['#field_name']; $parents = $element['#field_parents']; @@ -1101,20 +1175,104 @@ class ParagraphsWidget extends WidgetBase { if ($widget_state['real_item_count'] < $element['#cardinality'] || $element['#cardinality'] == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { $widget_state['items_count']++; + $widget_state['real_item_count']++; } + $position = $widget_state['real_item_count'] - 1; if (isset($button['#bundle_machine_name'])) { $widget_state['selected_bundle'] = $button['#bundle_machine_name']; } + elseif ($widget_state['add_mode'] == 'modal') { + $widget_state['selected_bundle'] = $element['add_more']['selection_form']['add_more_select']['#value']; + if ($element['add_more']['selection_form']['add_more_delta']['#value'] !== "") { + $position = $element['add_more']['selection_form']['add_more_delta']['#value']; + } + } else { $widget_state['selected_bundle'] = $element['add_more']['add_more_select']['#value']; } + // Create new paragraph entity. + $entity_type = \Drupal::entityTypeManager()->getDefinition('paragraph'); + $bundle_key = $entity_type->getKey('bundle'); + + $paragraphs_entity = \Drupal::entityTypeManager()->getStorage('paragraph')->create(array( + $bundle_key => $widget_state['selected_bundle'], + )); + + $paragraph = [ + 'entity' => $paragraphs_entity, + 'mode' => 'edit', + ]; + + if (isset($widget_state['paragraphs'][$position])) { + $paragraph['display'] = $widget_state['paragraphs'][$position]['display']; + } + + // Add paragraph to the widget state. + // This also creates it, when it doesn't exist. + $widget_state['paragraphs'][] = $paragraph; + + // If we use a modal, we have to place the new paragraph + // in the correct position. + if ($widget_state['add_mode'] == 'modal') { + // Clean form_state. + $user_input = $user_input_save = $form_state->getUserInput(); + + // Temporarily remove 'add_more' from array. + unset($user_input[$field_name]['add_more']); + + self::normalizeWeights($user_input[$field_name], $position); + + // It is important, that the key maps with the one from $widget_state + // The weight handles the position. + $key = count($widget_state['paragraphs']) - 1; + $item[$key] = [ + 'subform' => [], + '_weight' => $position, + ]; + + $user_input[$field_name] += $item; + + // Add back 'add_more'. + $user_input[$field_name]['add_more'] = $user_input_save[$field_name]; + + $form_state->setUserInput($user_input); + } + static::setWidgetState($parents, $field_name, $form_state, $widget_state); $form_state->setRebuild(); } + /** + * This resets the weights, ascending, starting from 0. + * + * It keeps the indices. + * + * E.g. [-6, -5, -4] will become [0, 1, 2] + * + * Starting at position, the elements will get weight+1, to make space + * for a new element. + * + * E.g. [-6, -5, -4], position = 1, will become [ 0, 2, 3] + * + * @param array $array + * The array, which weights should be normalised. + * @param int $position + * The position from which on the weights will be increased by 1. + */ + private static function normalizeWeights(array &$array, $position) { + $weigth = 0; + foreach ($array as &$value) { + if ($weigth == $position) { + $weigth++; + } + $value['_weight'] = $weigth; + $weigth++; + } + } + /** * Creates a duplicate of the paragraph entity. */ @@ -1487,6 +1645,21 @@ class ParagraphsWidget extends WidgetBase { return strip_tags($collapsed_summary_text); } + /** + * Returns render array for template of add paragraph modal dialog. + * + * @return array + * Render array. + */ + protected function getModalAddDialogTemplate() { + return [ + '#theme' => 'paragraphs_add_dialog', + '#attached' => [ + 'library' => ['paragraphs/drupal.paragraphs.modal'], + ], + ]; + } + /** * {@inheritdoc} */ diff --git a/templates/paragraphs-add-dialog.html.twig b/templates/paragraphs-add-dialog.html.twig new file mode 100644 index 0000000..0c7b5e8 --- /dev/null +++ b/templates/paragraphs-add-dialog.html.twig @@ -0,0 +1,28 @@ +{# +/** + * @file + * Default theme implementation of modal add paragraph dialog template. + * + * This template for dialog will be handled in javascript, where data-[key]="[attribute]" + * attributes will be replaced with data provided for that key. New attribute will be + * created like follows: [attribute]="[value-for-key]". + * + * Following classes have custom use: + * - paragraphs-add-dialog-template - is used to clone dialog. + * - paragraphs-add-dialog-row-template - is used to clone paragraph type rows. + * + * All listed classes will be renamed in final created dialog with "-template" sufix and + * they can be used with that name for theming. + * + * @ingroup themeable + */ +#} + +