diff --git a/js/layout-paragraphs-widget.js b/js/layout-paragraphs-widget.js index d5eacb61fe84e7d3fd6050dd4b2c998a3290875b..2bd20e739c91654607a43d34c64d57812ed9f471 100644 --- a/js/layout-paragraphs-widget.js +++ b/js/layout-paragraphs-widget.js @@ -43,6 +43,29 @@ ) => { setLoaded($(response.data.id)); }; + /** + * Ajax Command to insert or update a paragraph element. + * @param {object} ajax The ajax object. + * @param {object} response The response object. + */ + Drupal.AjaxCommands.prototype.layoutParagraphsInsert = (ajax, response) => { + const { settings, content } = response; + const weight = Math.floor(settings.weight); + const $container = settings.parent_selector + ? $(settings.parent_selector, settings.wrapper_selector) + : $(".active-items", settings.wrapper_selector); + const $sibling = $container.find( + `.layout-paragraphs-weight option[value="${weight}"]:selected` + ); + + if ($(settings.selector, settings.wrapper_selector).length) { + $(settings.selector, settings.wrapper_selector).replaceWith(content); + } else if ($sibling.length) { + $sibling.closest(".layout-paragraphs-item").after(content); + } else { + $container.prepend(content); + } + }; /** * The main layout-paragraphs Widget behavior. */ @@ -210,16 +233,6 @@ $(item).addClass("dragula-enabled"); // Turn on drag and drop if dragula function exists. if (typeof dragula !== "undefined") { - // Add layout handles. - $(".layout-paragraphs-item").each( - (layoutParagraphsItemIndex, layoutParagraphsItem) => { - $('<div class="layout-controls">') - .append($('<div class="layout-handle">')) - .append($('<div class="layout-up">').click(moveUp)) - .append($('<div class="layout-down">').click(moveDown)) - .prependTo(layoutParagraphsItem); - } - ); const items = $( ".active-items, .layout-paragraphs-layout-wrapper, .layout-paragraphs-layout-region, .layout-paragraphs-disabled-items__items", item @@ -435,9 +448,10 @@ } /** * Enhances the radio button select for choosing a layout. + * @param {Object} layoutList The list of layout items. */ - function enhanceRadioSelect() { - const $layoutRadioItem = $(".layout-select--list-item"); + function enhanceRadioSelect(layoutList) { + const $layoutRadioItem = $(".layout-select--list-item", layoutList); $layoutRadioItem.click(e => { const $radioItem = $(e.currentTarget); const $layoutParagraphsField = $radioItem.closest( @@ -581,68 +595,17 @@ }); }); /** - * Load entity form in dialog. + * Add drag/drop/move controls. */ - $(".layout-paragraphs-field .layout-paragraphs-form", context) - .once("layout-paragraphs-dialog") - .each((index, layoutParagraphsForm) => { - const buttons = []; - const $layoutParagraphsForm = $(layoutParagraphsForm); - $( - '.layout-paragraphs-item-form-actions input[type="submit"]', - layoutParagraphsForm - ).each((btnIndex, btn) => { - buttons.push({ - text: btn.value, - class: btn.className, - click() { - if ( - isLoading( - $layoutParagraphsForm.closest(".layout-paragraphs-field") - ) - ) { - return false; - } - setLoading($layoutParagraphsForm.closest(".ui-dialog")); - $(btn) - .trigger("mousedown") - .trigger("click"); - } - }); - btn.style.display = "none"; - }); - const dialogConfig = { - width: "800px", - title: $layoutParagraphsForm - .find("[data-dialog-title]") - .attr("data-dialog-title"), - maxHeight: Math.max(400, $(window).height() * 0.8), - minHeight: Math.min($layoutParagraphsForm.outerHeight(), 400), - appendTo: $(".layout-paragraphs-form").parent(), - draggable: true, - autoResize: true, - modal: true, - buttons, - open() { - enhanceRadioSelect(); - }, - beforeClose(event) { - if ( - isLoading($(event.target).closest(".layout-paragraphs-field")) - ) { - return false; - } - setLoading($(event.target).closest(".ui-dialog")); - $(event.target) - .find(".layout-paragraphs-cancel") - .trigger("mousedown") - .trigger("click"); - return false; - } - }; - $layoutParagraphsForm.dialog(dialogConfig); + $(".layout-paragraphs-item", context) + .once("layout-paragraphs-controls") + .each((layoutParagraphsItemIndex, layoutParagraphsItem) => { + $('<div class="layout-controls">') + .append($('<div class="layout-handle">')) + .append($('<div class="layout-up">').click(moveUp)) + .append($('<div class="layout-down">').click(moveDown)) + .prependTo(layoutParagraphsItem); }); - /** * Drag and drop with dragula. */ @@ -669,6 +632,14 @@ updateFields($(item)); updateDisabled($(item)); }); + /** + * Enhance radio buttons. + */ + $(".layout-select--list", context) + .once("layout-select-enhance-radios") + .each((index, layoutList) => { + enhanceRadioSelect(layoutList); + }); } }; })(jQuery, Drupal, dragula); diff --git a/src/Ajax/LayoutParagraphsInsertCommand.php b/src/Ajax/LayoutParagraphsInsertCommand.php new file mode 100644 index 0000000000000000000000000000000000000000..f3bb6f52b6361181e58323e708507583eb8ad877 --- /dev/null +++ b/src/Ajax/LayoutParagraphsInsertCommand.php @@ -0,0 +1,55 @@ +<?php + +namespace Drupal\layout_paragraphs\Ajax; + +use Drupal\Core\Ajax\CommandInterface; +use Drupal\Core\Ajax\CommandWithAttachedAssetsInterface; +use Drupal\Core\Ajax\CommandWithAttachedAssetsTrait; + +/** + * Class LayoutParagraphsStateResetCommand. + */ +class LayoutParagraphsInsertCommand implements CommandInterface, CommandWithAttachedAssetsInterface { + + use CommandWithAttachedAssetsTrait; + + /** + * The layout element settings array. + * + * @var array + */ + protected $settings; + + /** + * The content for the matched element(s). + * + * Either a render array or an HTML string. + * + * @var string|array + */ + protected $content; + + + /** + * Constructs a LayoutParagraphsInsertCommand instance. + */ + public function __construct($settings, $content) { + $this->settings = $settings; + $this->content = $content; + } + + /** + * Render custom ajax command. + * + * @return ajax + * Command function. + */ + public function render() { + return [ + 'command' => 'layoutParagraphsInsert', + 'content' => $this->getRenderedContent(), + 'settings' => $this->settings, + ]; + } + +} diff --git a/src/Ajax/LayoutParagraphsStateResetCommand.php b/src/Ajax/LayoutParagraphsStateResetCommand.php index 1f2a5bc19dcfd655316ac34cc9c77fc61b914d9c..9da2edca84af9c3a3ce3e7731964665ee4561a37 100644 --- a/src/Ajax/LayoutParagraphsStateResetCommand.php +++ b/src/Ajax/LayoutParagraphsStateResetCommand.php @@ -31,7 +31,7 @@ class LayoutParagraphsStateResetCommand implements CommandInterface { */ public function render() { return [ - 'command' => 'resetErlState', + 'command' => 'resetLayoutParagraphsState', 'data' => [ "id" => $this->id, ], diff --git a/src/Plugin/Field/FieldWidget/LayoutParagraphsWidget.php b/src/Plugin/Field/FieldWidget/LayoutParagraphsWidget.php index 6fccb2838c90a45250a58b9bc3d103419d4960d1..b8d81bb79121a4e0787dcf4853e2664302081b91 100644 --- a/src/Plugin/Field/FieldWidget/LayoutParagraphsWidget.php +++ b/src/Plugin/Field/FieldWidget/LayoutParagraphsWidget.php @@ -26,9 +26,15 @@ use Drupal\Core\Field\FieldFilteredMarkup; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\OpenDialogCommand; +use Drupal\Core\Ajax\AppendCommand; +use Drupal\Core\Ajax\PrependCommand; +use Drupal\Core\Ajax\RemoveCommand; +use Drupal\Core\Ajax\CloseDialogCommand; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\paragraphs\ParagraphInterface; use Drupal\layout_paragraphs\Ajax\LayoutParagraphsStateResetCommand; +use Drupal\layout_paragraphs\Ajax\LayoutParagraphsInsertCommand; /** * Entity Reference with Layout field widget. @@ -331,8 +337,7 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi '#limit_validation_errors' => [array_merge($parents, [$this->fieldName])], '#attributes' => ['class' => ['layout-paragraphs-add-item']], '#ajax' => [ - 'callback' => [$this, 'elementAjax'], - 'wrapper' => $this->wrapperId, + 'callback' => [$this, 'editItemAjax'], ], '#name' => implode('_', $parents) . '_add_item', '#element_parents' => $parents, @@ -452,6 +457,10 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi return []; } + if (isset($widget_state_item['is_new'])) { + return; + } + /** @var \Drupal\paragraphs\ParagraphInterface $entity */ $entity = $widget_state_item['entity']; $layout_settings = $entity->getAllBehaviorSettings()['layout_paragraphs'] ?? []; @@ -475,6 +484,7 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId()); $preview = $view_builder->view($entity, $preview_view_mode); $preview['#cache']['max-age'] = 0; + $preview['#attributes']['class'][] = Html::cleanCssIdentifier($entity->uuid() . '-preview'); } $element = [ @@ -489,6 +499,7 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi '#attributes' => [ 'class' => [ 'layout-paragraphs-item', + 'paragraph-' . $entity->uuid(), ], 'id' => [ $this->fieldName . '--item-' . $delta, @@ -511,7 +522,7 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi 'parent_uuid' => [ '#type' => 'hidden', '#attributes' => ['class' => ['layout-paragraphs-parent-uuid']], - '#value' => $parent_uuid, + '#default_value' => $parent_uuid, ], 'entity' => [ '#type' => 'value', @@ -532,8 +543,7 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi '#submit' => [[$this, 'editItemSubmit']], '#delta' => $delta, '#ajax' => [ - 'callback' => [$this, 'elementAjax'], - 'wrapper' => $this->wrapperId, + 'callback' => [$this, 'editItemAjax'], 'progress' => 'none', ], '#element_parents' => $parents, @@ -547,8 +557,7 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi '#submit' => [[$this, 'removeItemSubmit']], '#delta' => $delta, '#ajax' => [ - 'callback' => [$this, 'elementAjax'], - 'wrapper' => $this->wrapperId, + 'callback' => [$this, 'removeItemAjax'], 'progress' => 'none', ], '#element_parents' => $parents, @@ -576,7 +585,10 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi // New items are rendered in layout but hidden. // This way we can track their weights, region names, etc. if (!empty($widget_state['items'][$delta]['is_new'])) { - $element['#attributes']['class'][] = 'js-hide'; + $element['#is_new'] = TRUE; + } + else { + $element['#is_new'] = FALSE; } return $element; @@ -596,10 +608,13 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi public function buildLayouts(array $elements, FormStateInterface $form_state) { $tree = []; $paragraph_elements = []; + $elements['#items'] = []; foreach (Element::children($elements) as $index) { $element = $elements[$index]; if (!empty($element['#widget_item'])) { $paragraph_elements[] = $element; + // Maintain a hidden flast list of elements to easily locate items. + $elements['#items'][$element['#entity']->uuid()] = $element; unset($elements[$index]); } } @@ -879,20 +894,21 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi $element['entity_form'] += [ 'actions' => [ '#weight' => 1000, - '#type' => 'container', + '#type' => 'actions', '#attributes' => ['class' => ['layout-paragraphs-item-form-actions']], 'save_item' => [ '#type' => 'submit', '#name' => 'save', '#value' => $this->t('Save'), '#delta' => $delta, + '#uuid' => $entity->uuid(), '#limit_validation_errors' => [array_merge($parents, [$this->fieldName])], '#submit' => [ [$this, 'saveItemSubmit'], ], '#ajax' => [ - 'callback' => [$this, 'elementAjax'], - 'wrapper' => $this->wrapperId, + 'callback' => [$this, 'saveItemAjax'], + //'wrapper' => $this->wrapperId, 'progress' => 'none', ], '#element_parents' => $parents, @@ -958,10 +974,10 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi '#type' => 'submit', '#value' => $this->t('Remove'), '#delta' => $delta, + '#uuid' => $entity->uuid(), '#submit' => [[$this, 'removeItemConfirmSubmit']], '#ajax' => [ - 'callback' => [$this, 'elementAjax'], - 'wrapper' => $this->wrapperId, + 'callback' => [$this, 'removeItemConfirmAjax'], 'progress' => 'none', ], '#element_parents' => $parents, @@ -1158,12 +1174,22 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi public function removeItemConfirmSubmit($form, $form_state) { $element = $form_state->getTriggeringElement(); + $uuid = $element['#uuid']; $parents = $element['#element_parents']; $delta = $element['#delta']; $widget_state = static::getWidgetState($parents, $this->fieldName, $form_state); unset($widget_state['items'][$delta]); + foreach ($widget_state['items'] as $delta => $item) { + /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ + $paragraph = $item['entity']; + $behavior_settings = $paragraph->getAllBehaviorSettings()['layout_paragraphs']; + if (isset($behavior_settings['parent_uuid']) && $behavior_settings['parent_uuid'] == $uuid) { + unset($widget_state['items'][$delta]); + } + + } $widget_state['remove_item'] = FALSE; static::setWidgetState($parents, $this->fieldName, $form_state, $widget_state); @@ -1277,13 +1303,150 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi $parents = $element['#element_parents']; $field_state = static::getWidgetState($parents, $this->fieldName, $form_state); $widget_field = NestedArray::getValue($form, $field_state['array_parents']); + $html_id = $this->entityFormHtmlId($field_state); $response = new AjaxResponse(); + $response->addCommand(new CloseDialogCommand('#' . $html_id)); $response->addCommand(new ReplaceCommand('#' . $this->wrapperId, $widget_field)); $response->addCommand(new LayoutParagraphsStateResetCommand('#' . $this->wrapperId)); return $response; } + /** + * Ajax callback to return the entire ERL element. + */ + public function saveItemAjax(array $form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $uuid = $triggering_element['#uuid']; + $parents = $triggering_element['#element_parents']; + $field_state = static::getWidgetState($parents, $this->fieldName, $form_state); + $widget_field = NestedArray::getValue($form, $field_state['array_parents']); + $html_id = $this->entityFormHtmlId($field_state); + + $element = static::findElementByUuid($widget_field['active_items']['items'], $uuid); + /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */ + $paragraph = $element['#entity']; + $behavior_settings = $paragraph->getAllBehaviorSettings()['layout_paragraphs']; + + if ($behavior_settings['parent_uuid'] && $behavior_settings['region']) { + $parent_selector = '.paragraph-' . $behavior_settings['parent_uuid'] . ' .layout-paragraphs-layout-region--' . $behavior_settings['region']; + } + else { + $parent_selector = ''; + } + + $settings = [ + 'wrapper_selector' => '#' . $this->wrapperId, + 'selector' => '.paragraph-' . $uuid, + 'parent_selector' => $parent_selector, + 'weight' => $element['#weight'], + ]; + + $response = new AjaxResponse(); + $response->addCommand(new LayoutParagraphsInsertCommand($settings, $element)); + $response->addCommand(new CloseDialogCommand('#' . $html_id)); + return $response; + } + + /** + * Recursively search the build array for element with matching uuid. + * + * @param array $array + * Nested build array. + * @param string $uuid + * The uuid of the element to find. + * + * @return array + * The matching element build array. + */ + public static function findElementByUuid(array $array, string $uuid) { + $element = FALSE; + foreach ($array as $key => $item) { + if (is_array($item)) { + if (isset($item['#entity'])) { + if ($item['#entity']->uuid() == $uuid) { + return $item; + } + } + if (isset($item['preview']['regions'])) { + foreach (Element::children($item['preview']['regions']) as $region_name) { + if ($element = static::findElementByUuid($item['preview']['regions'][$region_name], $uuid)) { + return $element; + } + } + } + } + } + return $element; + } + + /** + * Ajax callback to return the entire ERL element. + */ + public function editItemAjax(array $form, FormStateInterface $form_state) { + $element = $form_state->getTriggeringElement(); + $parents = $element['#element_parents']; + $field_state = static::getWidgetState($parents, $this->fieldName, $form_state); + $widget_field = NestedArray::getValue($form, $field_state['array_parents']); + $entity_form = $widget_field['entity_form']; + $html_id = $this->entityFormHtmlId($field_state); + + $dialog_options = [ + 'modal' => TRUE, + 'appendTo' => '#' . $this->wrapperId, + 'width' => 800, + 'drupalAutoButtons' => TRUE, + ]; + + $response = new AjaxResponse(); + $response->addCommand(new AppendCommand('#' . $this->wrapperId, '<div id="' . $html_id . '"></div>')); + $response->addCommand(new OpenDialogCommand('#' . $html_id, 'Edit Form', $entity_form, $dialog_options)); + $response->addCommand(new LayoutParagraphsStateResetCommand('#' . $this->wrapperId)); + return $response; + } + + /** + * Ajax callback to remove an item - launches confirmation dialog. + */ + public function removeItemAjax(array $form, FormStateInterface $form_state) { + $element = $form_state->getTriggeringElement(); + $parents = $element['#element_parents']; + $field_state = static::getWidgetState($parents, $this->fieldName, $form_state); + $widget_field = NestedArray::getValue($form, $field_state['array_parents']); + $entity_form = $widget_field['remove_form']; + $html_id = $this->entityFormHtmlId($field_state); + + $dialog_options = [ + 'modal' => TRUE, + 'appendTo' => '#' . $this->wrapperId, + 'width' => 800, + 'drupalAutoButtons' => TRUE, + ]; + + $response = new AjaxResponse(); + $response->addCommand(new AppendCommand('#' . $this->wrapperId, '<div id="' . $html_id . '"></div>')); + $response->addCommand(new OpenDialogCommand('#' . $html_id, 'Edit Form', $entity_form, $dialog_options)); + $response->addCommand(new LayoutParagraphsStateResetCommand('#' . $this->wrapperId)); + return $response; + } + + + /** + * Ajax callback to remove an item - removes item from DOM. + */ + public function removeItemConfirmAjax(array $form, FormStateInterface $form_state) { + $element = $form_state->getTriggeringElement(); + $uuid = $element['#uuid']; + $parents = $element['#element_parents']; + $field_state = static::getWidgetState($parents, $this->fieldName, $form_state); + $html_id = $this->entityFormHtmlId($field_state); + + $response = new AjaxResponse(); + $response->addCommand(new RemoveCommand('.paragraph-' . $uuid)); + $response->addCommand(new CloseDialogCommand('#' . $html_id)); + return $response; + } + /** * Ajax callback to return a layout plugin configuration form. */ @@ -1304,6 +1467,19 @@ class LayoutParagraphsWidget extends WidgetBase implements ContainerFactoryPlugi } } + /** + * Generates an ID for the entity form dialog container. + * + * @param array $field_state + * The field state with array_parents. + * + * @return string + * The HTML id. + */ + private function entityFormHtmlId(array $field_state) { + return trim(Html::getId(implode('-', $field_state['array_parents']) . '-entity-form'), '-'); + } + /** * Field instance settings form. */