Loading js/builder.es6.js +4 −7 Original line number Diff line number Diff line Loading @@ -10,7 +10,8 @@ * @param {Object} settings * The settings object. */ function attachUiElements($container, id, settings) { function attachUiElements($container, settings) { const id = $container[0].id; const lpbBuilderSettings = settings.lpBuilder || {}; const uiElements = lpbBuilderSettings.uiElements || {}; const containerUiElements = uiElements[id] || []; Loading Loading @@ -441,14 +442,10 @@ Drupal.behaviors.layoutParagraphsBuilder = { attach: function attach(context, settings) { // Add UI elements to the builder, each component, and each region. [`${idAttr}`, 'data-uuid', 'data-region-uuid'].forEach((attr) => { $(`[${attr}]`) .not('.lpb-formatter') .not('.has-components') $('[data-has-js-ui-element]') .once('lpb-ui-elements') .each((i, el) => { attachUiElements($(el), el.getAttribute(attr), settings); }); attachUiElements($(el), settings); }); // Listen to relevant events and update UI. const events = [ Loading js/builder.js +4 −5 Original line number Diff line number Diff line Loading @@ -20,7 +20,8 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } (function ($, Drupal, debounce, dragula) { var idAttr = 'data-lpb-id'; function attachUiElements($container, id, settings) { function attachUiElements($container, settings) { var id = $container[0].id; var lpbBuilderSettings = settings.lpBuilder || {}; var uiElements = lpbBuilderSettings.uiElements || {}; var containerUiElements = uiElements[id] || []; Loading Loading @@ -354,10 +355,8 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } Drupal.behaviors.layoutParagraphsBuilder = { attach: function attach(context, settings) { ["".concat(idAttr), 'data-uuid', 'data-region-uuid'].forEach(function (attr) { $("[".concat(attr, "]")).not('.lpb-formatter').not('.has-components').once('lpb-ui-elements').each(function (i, el) { attachUiElements($(el), el.getAttribute(attr), settings); }); $('[data-has-js-ui-element]').once('lpb-ui-elements').each(function (i, el) { attachUiElements($(el), settings); }); var events = ['lpb-builder:init.lpb', 'lpb-component:insert.lpb', 'lpb-component:update.lpb', 'lpb-component:move.lpb', 'lpb-component:drop.lpb', 'lpb-component:delete.lpb'].join(' '); $('[data-lpb-id]').once('lpb-events').on(events, function (e) { Loading src/Element/LayoutParagraphsBuilder.php +52 −24 Original line number Diff line number Diff line Loading @@ -5,14 +5,15 @@ namespace Drupal\layout_paragraphs\Element; use Drupal\Core\Url; use Drupal\Core\Render\Markup; use Drupal\Core\Render\Renderer; use Drupal\Component\Utility\Html; use Drupal\Component\Serialization\Json; use Drupal\paragraphs\ParagraphInterface; use Drupal\Core\Access\AccessResultAllowed; use Drupal\Core\Layout\LayoutPluginManager; use Drupal\Core\Entity\EntityTypeBundleInfo; use Drupal\layout_paragraphs\Utility\Dialog; use Drupal\Core\Access\AccessResultForbidden; use Drupal\Core\Render\Element\RenderElement; use Drupal\layout_paragraphs\Utility\Dialog; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\layout_paragraphs\LayoutParagraphsSection; Loading Loading @@ -223,16 +224,26 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP 'lp-builder', 'lp-builder-' . $this->layoutParagraphsLayout->id(), ], 'id' => Html::getUniqueId($this->layoutParagraphsLayout->id()), 'data-lpb-id' => $this->layoutParagraphsLayout->id(), ]; $element['#attached']['library'] = ['layout_paragraphs/builder']; $element['#attached']['drupalSettings']['lpBuilder'][$this->layoutParagraphsLayout->id()] = $this->layoutParagraphsLayout->getSettings(); $element['#is_empty'] = $this->layoutParagraphsLayout->isEmpty(); $element['#empty_message'] = $this->layoutParagraphsLayout->getSetting('empty_message', $this->t('Start adding content.')); $element['#root_components'] = []; foreach ($this->layoutParagraphsLayout->getRootComponents() as $component) { /** @var \Drupal\layout_paragraphs\LayoutParagraphsComponent $component */ $uuid = $component->getEntity()->uuid(); $element['#root_components'][$uuid] =& $element['#components'][$uuid]; } if (count($element['#root_components'])) { $element['#attributes']['class'][] = 'has-components'; } else { if ($this->layoutParagraphsLayout->getSetting('require_layouts', FALSE)) { $this->addJsUiElement( $element, $this->layoutParagraphsLayout->id(), $this->doRender($this->insertSectionButton(['layout_paragraphs_layout' => $this->layoutParagraphsLayout->id()], [], 0, ['center'])), 'insert' ); Loading @@ -240,19 +251,10 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP else { $this->addJsUiElement( $element, $this->layoutParagraphsLayout->id(), $this->doRender($this->insertComponentButton(['layout_paragraphs_layout' => $this->layoutParagraphsLayout->id()], [], 0, ['center'])), 'insert' ); } $element['#root_components'] = []; foreach ($this->layoutParagraphsLayout->getRootComponents() as $component) { /** @var \Drupal\layout_paragraphs\LayoutParagraphsComponent $component */ $uuid = $component->getEntity()->uuid(); $element['#root_components'][$uuid] =& $element['#components'][$uuid]; } if (count($element['#root_components'])) { $element['#attributes']['class'][] = 'has-components'; } return $element; } Loading @@ -279,6 +281,7 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP $build['#attributes']['data-type'] = $entity->bundle(); $build['#attributes']['data-id'] = $entity->id(); $build['#attributes']['class'][] = 'js-lpb-component'; $build['#attributes']['id'] = Html::getUniqueId($entity->id()); $build['#layout_paragraphs_component'] = TRUE; if ($entity->isNew()) { $build['#attributes']['class'][] = 'is_new'; Loading Loading @@ -307,23 +310,22 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP '#uuid' => $entity->uuid(), '#layout_paragraphs_layout' => $this->layoutParagraphsLayout, '#edit_access' => $this->editAccess($entity), '#duplicate_access' => $this->createAccess(), '#duplicate_access' => $this->createAccess() && $this->checkCardinality(), '#delete_access' => $this->deleteAccess($entity), ]; $this->addJsUiElement($build, $entity->uuid(), $this->doRender($controls), 'controls', 'prepend'); $build['#attached']['drupalSettings']['lpBuilder']['uiElements'][$entity->uuid()] = []; $this->addJsUiElement($build, $this->doRender($controls), 'controls', 'prepend'); if ($this->createAccess()) { if ($this->createAccess() && $this->checkCardinality()) { if (!$component->getParentUuid() && $this->layoutParagraphsLayout->getSetting('require_layouts')) { $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertSectionButton($url_params, $query_params + ['placement' => 'before'], -10000, ['before'])), 'insert_before', 'prepend' ); $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertSectionButton($url_params, $query_params + ['placement' => 'after'], 10000, ['after'])), 'insert_after', 'append' Loading @@ -332,14 +334,12 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP else { $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertComponentButton($url_params, $query_params + ['placement' => 'before'], -10000, ['before'])), 'insert_before', 'prepend' ); $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertComponentButton($url_params, $query_params + ['placement' => 'after'], -10000, ['after'])), 'insert_after', 'append' Loading Loading @@ -371,12 +371,12 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP ], 'data-region' => $region_name, 'data-region-uuid' => $entity->uuid() . '-' . $region_name, 'id' => Html::getUniqueId($entity->uuid() . '-' . $region_name), ], ]; if ($this->createAccess()) { if ($this->createAccess() && $this->checkCardinality()) { $this->addJsUiElement( $build, $entity->uuid() . '-' . $region_name, $build['regions'][$region_name], $this->doRender($this->insertComponentButton($url_params, $query_params, 10000, ['center'])), 'insert' ); Loading Loading @@ -681,8 +681,6 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP * * @param array $build * The build array to attach JS settings to. * @param string $id * The element container's id. * @param \Drupal\Core\Render\Markup $element * The UI element. * @param string $key Loading @@ -690,7 +688,9 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP * @param string $method * The javascript method to use to attach $element to its container. */ public function addJsUiElement(array &$build, string $id, Markup $element, string $key, string $method = 'append') { public function addJsUiElement(array &$build, Markup $element, string $key, string $method = 'append') { $id = $build['#attributes']['id']; $build['#attributes']['data-has-js-ui-element'] = TRUE; $build['#attached']['drupalSettings']['lpBuilder']['uiElements'][$id][$key] = [ 'element' => $element, 'method' => $method, Loading @@ -710,4 +710,32 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP return $this->renderer->render($render_array); } /** * Checks if adding a component would exceed the field's cardinality limit. * * @return bool * True if a compoment can be added without exceeding cardinality. */ protected function checkCardinality() { $cardinality = $this->getCardinality(); if ($cardinality > 0) { $count = $this->layoutParagraphsLayout->getParagraphsReferenceField()->count(); return $cardinality > $count; } return TRUE; } /** * Gets the cardinality field setting for a Layout Paragraphs reference field. * * @return int * The cardinality setting. */ protected function getCardinality() { $field_name = $this->layoutParagraphsLayout->getFieldName(); $field_config = $this->layoutParagraphsLayout->getEntity()->{$field_name}->getFieldDefinition(); $field_definition = $field_config->getFieldStorageDefinition(); return $field_definition->getCardinality(); } } src/EventSubscriber/LayoutParagraphsUpdateLayoutSubscriber.php +47 −5 Original line number Diff line number Diff line Loading @@ -15,22 +15,64 @@ class LayoutParagraphsUpdateLayoutSubscriber implements EventSubscriberInterface */ public static function getSubscribedEvents() { return [ LayoutParagraphsUpdateLayoutEvent::EVENT_NAME => 'compareLayouts', LayoutParagraphsUpdateLayoutEvent::EVENT_NAME => 'layoutUpdated', ]; } /** * Restricts available types based on settings in layout. * Determines if a Layout Paragraphs Builder UI needs to be refreshed. * * Some interactions will create conditions where the entire builder UI needs * to be refreshed, rather than simply returning a single new or edited * component. For example: when removing the only component from a layout, * the layout should be refreshed to show the correct ui/messaging for an * empty container. * * @param \Drupal\layout_paragraphs\Event\LayoutParagraphsUpdateLayoutEvent $event * The allowed types event. * The event. */ public function compareLayouts(LayoutParagraphsUpdateLayoutEvent $event) { public function layoutUpdated(LayoutParagraphsUpdateLayoutEvent $event) { $event->needsRefresh = $this->compareEmptyState($event) || $this->compareMaximumReached($event); } /** * Compares the empty state of the original and updated layout. * * If the original layout was empty and the updated layout is not, or visa * versa, the entire layout builder ui needs to be refreshed. * * @param \Drupal\layout_paragraphs\Event\LayoutParagraphsUpdateLayoutEvent $event * The event. */ public function compareEmptyState(LayoutParagraphsUpdateLayoutEvent $event) { $original = $event->getOriginalLayout()->getParagraphsReferenceField(); $layout = $event->getUpdatedLayout()->getParagraphsReferenceField(); return $original->isEmpty() != $layout->isEmpty(); } /** * Compares count == cardinality of the original and updated layouts. * * @param \Drupal\layout_paragraphs\Event\LayoutParagraphsUpdateLayoutEvent $event * The event. * * @return bool * True if the count == cardinality limit has changed. */ public function compareMaximumReached(LayoutParagraphsUpdateLayoutEvent $event) { $original_count = $event->getOriginalLayout()->getParagraphsReferenceField()->count(); $updated_count = $event->getUpdatedLayout()->getParagraphsReferenceField()->count(); $cardinality = $event->getUpdatedLayout() ->getParagraphsReferenceField() ->getFieldDefinition() ->getFieldStorageDefinition() ->getCardinality(); $original_is_max = $cardinality > 0 && $cardinality <= $original_count; $updated_is_max = $cardinality > 0 && $cardinality <= $updated_count; $event->needsRefresh = ($original->isEmpty() != $layout->isEmpty()); return $original_is_max != $updated_is_max; } } tests/src/FunctionalJavascript/CardinalityTest.php 0 → 100644 +70 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\layout_paragraphs\FunctionalJavascript; /** * Tests cardinality settings for a Layout Paragraphs field widget. * * @group layout_paragraphs */ class CardinalityTest extends BuilderTestBase { /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->loginWithPermissions([ 'administer site configuration', 'administer node fields', 'administer node display', 'administer paragraphs types', ]); $this->drupalGet('admin/structure/types/manage/page/fields/node.page.field_content/storage'); $this->submitForm([ 'cardinality' => 'number', 'cardinality_number' => 2, ], 'Save field settings'); } /** * Tests cardinality settings for a Layout Paragraphs field widget. */ public function testCardinality() { $this->loginWithPermissions([ 'create page content', 'edit own page content', ]); $this->drupalGet('node/add/page'); $page = $this->getSession()->getPage(); // Add a three-column section. $this->addSectionComponent(2, '.lpb-btn--add'); // Cardinality is set to 2. We should still have (+) buttons. $this->assertSession()->elementExists('css', '.layout__region--first .lpb-btn--add'); $this->htmlOutput($this->getSession()->getPage()->getHtml()); // Add a text component. $this->addTextComponent('Some arbitrary text', '.layout__region--first .lpb-btn--add'); // Maximum number has been reached. There should be no more (+) buttons. $this->assertSession()->elementNotExists('css', '.layout__region--first .lpb-btn--add'); $this->htmlOutput($this->getSession()->getPage()->getHtml()); // Remove a component. $button = $page->find('css', '.layout__region--first a.lpb-delete'); $button->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $button = $page->find('css', 'button.lpb-btn--confirm-delete'); $button->click(); $this->assertSession()->assertWaitOnAjaxRequest(); // We no longer have the maximum allowed items, and should have (+) buttons. $this->assertSession()->elementExists('css', '.layout__region--first .lpb-btn--add'); $this->htmlOutput($this->getSession()->getPage()->getHtml()); } } js/builder-form.js +1 −1 File changed.Contains only whitespace changes. Show changes Loading
js/builder.es6.js +4 −7 Original line number Diff line number Diff line Loading @@ -10,7 +10,8 @@ * @param {Object} settings * The settings object. */ function attachUiElements($container, id, settings) { function attachUiElements($container, settings) { const id = $container[0].id; const lpbBuilderSettings = settings.lpBuilder || {}; const uiElements = lpbBuilderSettings.uiElements || {}; const containerUiElements = uiElements[id] || []; Loading Loading @@ -441,14 +442,10 @@ Drupal.behaviors.layoutParagraphsBuilder = { attach: function attach(context, settings) { // Add UI elements to the builder, each component, and each region. [`${idAttr}`, 'data-uuid', 'data-region-uuid'].forEach((attr) => { $(`[${attr}]`) .not('.lpb-formatter') .not('.has-components') $('[data-has-js-ui-element]') .once('lpb-ui-elements') .each((i, el) => { attachUiElements($(el), el.getAttribute(attr), settings); }); attachUiElements($(el), settings); }); // Listen to relevant events and update UI. const events = [ Loading
js/builder.js +4 −5 Original line number Diff line number Diff line Loading @@ -20,7 +20,8 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } (function ($, Drupal, debounce, dragula) { var idAttr = 'data-lpb-id'; function attachUiElements($container, id, settings) { function attachUiElements($container, settings) { var id = $container[0].id; var lpbBuilderSettings = settings.lpBuilder || {}; var uiElements = lpbBuilderSettings.uiElements || {}; var containerUiElements = uiElements[id] || []; Loading Loading @@ -354,10 +355,8 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } Drupal.behaviors.layoutParagraphsBuilder = { attach: function attach(context, settings) { ["".concat(idAttr), 'data-uuid', 'data-region-uuid'].forEach(function (attr) { $("[".concat(attr, "]")).not('.lpb-formatter').not('.has-components').once('lpb-ui-elements').each(function (i, el) { attachUiElements($(el), el.getAttribute(attr), settings); }); $('[data-has-js-ui-element]').once('lpb-ui-elements').each(function (i, el) { attachUiElements($(el), settings); }); var events = ['lpb-builder:init.lpb', 'lpb-component:insert.lpb', 'lpb-component:update.lpb', 'lpb-component:move.lpb', 'lpb-component:drop.lpb', 'lpb-component:delete.lpb'].join(' '); $('[data-lpb-id]').once('lpb-events').on(events, function (e) { Loading
src/Element/LayoutParagraphsBuilder.php +52 −24 Original line number Diff line number Diff line Loading @@ -5,14 +5,15 @@ namespace Drupal\layout_paragraphs\Element; use Drupal\Core\Url; use Drupal\Core\Render\Markup; use Drupal\Core\Render\Renderer; use Drupal\Component\Utility\Html; use Drupal\Component\Serialization\Json; use Drupal\paragraphs\ParagraphInterface; use Drupal\Core\Access\AccessResultAllowed; use Drupal\Core\Layout\LayoutPluginManager; use Drupal\Core\Entity\EntityTypeBundleInfo; use Drupal\layout_paragraphs\Utility\Dialog; use Drupal\Core\Access\AccessResultForbidden; use Drupal\Core\Render\Element\RenderElement; use Drupal\layout_paragraphs\Utility\Dialog; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\layout_paragraphs\LayoutParagraphsSection; Loading Loading @@ -223,16 +224,26 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP 'lp-builder', 'lp-builder-' . $this->layoutParagraphsLayout->id(), ], 'id' => Html::getUniqueId($this->layoutParagraphsLayout->id()), 'data-lpb-id' => $this->layoutParagraphsLayout->id(), ]; $element['#attached']['library'] = ['layout_paragraphs/builder']; $element['#attached']['drupalSettings']['lpBuilder'][$this->layoutParagraphsLayout->id()] = $this->layoutParagraphsLayout->getSettings(); $element['#is_empty'] = $this->layoutParagraphsLayout->isEmpty(); $element['#empty_message'] = $this->layoutParagraphsLayout->getSetting('empty_message', $this->t('Start adding content.')); $element['#root_components'] = []; foreach ($this->layoutParagraphsLayout->getRootComponents() as $component) { /** @var \Drupal\layout_paragraphs\LayoutParagraphsComponent $component */ $uuid = $component->getEntity()->uuid(); $element['#root_components'][$uuid] =& $element['#components'][$uuid]; } if (count($element['#root_components'])) { $element['#attributes']['class'][] = 'has-components'; } else { if ($this->layoutParagraphsLayout->getSetting('require_layouts', FALSE)) { $this->addJsUiElement( $element, $this->layoutParagraphsLayout->id(), $this->doRender($this->insertSectionButton(['layout_paragraphs_layout' => $this->layoutParagraphsLayout->id()], [], 0, ['center'])), 'insert' ); Loading @@ -240,19 +251,10 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP else { $this->addJsUiElement( $element, $this->layoutParagraphsLayout->id(), $this->doRender($this->insertComponentButton(['layout_paragraphs_layout' => $this->layoutParagraphsLayout->id()], [], 0, ['center'])), 'insert' ); } $element['#root_components'] = []; foreach ($this->layoutParagraphsLayout->getRootComponents() as $component) { /** @var \Drupal\layout_paragraphs\LayoutParagraphsComponent $component */ $uuid = $component->getEntity()->uuid(); $element['#root_components'][$uuid] =& $element['#components'][$uuid]; } if (count($element['#root_components'])) { $element['#attributes']['class'][] = 'has-components'; } return $element; } Loading @@ -279,6 +281,7 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP $build['#attributes']['data-type'] = $entity->bundle(); $build['#attributes']['data-id'] = $entity->id(); $build['#attributes']['class'][] = 'js-lpb-component'; $build['#attributes']['id'] = Html::getUniqueId($entity->id()); $build['#layout_paragraphs_component'] = TRUE; if ($entity->isNew()) { $build['#attributes']['class'][] = 'is_new'; Loading Loading @@ -307,23 +310,22 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP '#uuid' => $entity->uuid(), '#layout_paragraphs_layout' => $this->layoutParagraphsLayout, '#edit_access' => $this->editAccess($entity), '#duplicate_access' => $this->createAccess(), '#duplicate_access' => $this->createAccess() && $this->checkCardinality(), '#delete_access' => $this->deleteAccess($entity), ]; $this->addJsUiElement($build, $entity->uuid(), $this->doRender($controls), 'controls', 'prepend'); $build['#attached']['drupalSettings']['lpBuilder']['uiElements'][$entity->uuid()] = []; $this->addJsUiElement($build, $this->doRender($controls), 'controls', 'prepend'); if ($this->createAccess()) { if ($this->createAccess() && $this->checkCardinality()) { if (!$component->getParentUuid() && $this->layoutParagraphsLayout->getSetting('require_layouts')) { $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertSectionButton($url_params, $query_params + ['placement' => 'before'], -10000, ['before'])), 'insert_before', 'prepend' ); $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertSectionButton($url_params, $query_params + ['placement' => 'after'], 10000, ['after'])), 'insert_after', 'append' Loading @@ -332,14 +334,12 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP else { $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertComponentButton($url_params, $query_params + ['placement' => 'before'], -10000, ['before'])), 'insert_before', 'prepend' ); $this->addJsUiElement( $build, $entity->uuid(), $this->doRender($this->insertComponentButton($url_params, $query_params + ['placement' => 'after'], -10000, ['after'])), 'insert_after', 'append' Loading Loading @@ -371,12 +371,12 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP ], 'data-region' => $region_name, 'data-region-uuid' => $entity->uuid() . '-' . $region_name, 'id' => Html::getUniqueId($entity->uuid() . '-' . $region_name), ], ]; if ($this->createAccess()) { if ($this->createAccess() && $this->checkCardinality()) { $this->addJsUiElement( $build, $entity->uuid() . '-' . $region_name, $build['regions'][$region_name], $this->doRender($this->insertComponentButton($url_params, $query_params, 10000, ['center'])), 'insert' ); Loading Loading @@ -681,8 +681,6 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP * * @param array $build * The build array to attach JS settings to. * @param string $id * The element container's id. * @param \Drupal\Core\Render\Markup $element * The UI element. * @param string $key Loading @@ -690,7 +688,9 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP * @param string $method * The javascript method to use to attach $element to its container. */ public function addJsUiElement(array &$build, string $id, Markup $element, string $key, string $method = 'append') { public function addJsUiElement(array &$build, Markup $element, string $key, string $method = 'append') { $id = $build['#attributes']['id']; $build['#attributes']['data-has-js-ui-element'] = TRUE; $build['#attached']['drupalSettings']['lpBuilder']['uiElements'][$id][$key] = [ 'element' => $element, 'method' => $method, Loading @@ -710,4 +710,32 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP return $this->renderer->render($render_array); } /** * Checks if adding a component would exceed the field's cardinality limit. * * @return bool * True if a compoment can be added without exceeding cardinality. */ protected function checkCardinality() { $cardinality = $this->getCardinality(); if ($cardinality > 0) { $count = $this->layoutParagraphsLayout->getParagraphsReferenceField()->count(); return $cardinality > $count; } return TRUE; } /** * Gets the cardinality field setting for a Layout Paragraphs reference field. * * @return int * The cardinality setting. */ protected function getCardinality() { $field_name = $this->layoutParagraphsLayout->getFieldName(); $field_config = $this->layoutParagraphsLayout->getEntity()->{$field_name}->getFieldDefinition(); $field_definition = $field_config->getFieldStorageDefinition(); return $field_definition->getCardinality(); } }
src/EventSubscriber/LayoutParagraphsUpdateLayoutSubscriber.php +47 −5 Original line number Diff line number Diff line Loading @@ -15,22 +15,64 @@ class LayoutParagraphsUpdateLayoutSubscriber implements EventSubscriberInterface */ public static function getSubscribedEvents() { return [ LayoutParagraphsUpdateLayoutEvent::EVENT_NAME => 'compareLayouts', LayoutParagraphsUpdateLayoutEvent::EVENT_NAME => 'layoutUpdated', ]; } /** * Restricts available types based on settings in layout. * Determines if a Layout Paragraphs Builder UI needs to be refreshed. * * Some interactions will create conditions where the entire builder UI needs * to be refreshed, rather than simply returning a single new or edited * component. For example: when removing the only component from a layout, * the layout should be refreshed to show the correct ui/messaging for an * empty container. * * @param \Drupal\layout_paragraphs\Event\LayoutParagraphsUpdateLayoutEvent $event * The allowed types event. * The event. */ public function compareLayouts(LayoutParagraphsUpdateLayoutEvent $event) { public function layoutUpdated(LayoutParagraphsUpdateLayoutEvent $event) { $event->needsRefresh = $this->compareEmptyState($event) || $this->compareMaximumReached($event); } /** * Compares the empty state of the original and updated layout. * * If the original layout was empty and the updated layout is not, or visa * versa, the entire layout builder ui needs to be refreshed. * * @param \Drupal\layout_paragraphs\Event\LayoutParagraphsUpdateLayoutEvent $event * The event. */ public function compareEmptyState(LayoutParagraphsUpdateLayoutEvent $event) { $original = $event->getOriginalLayout()->getParagraphsReferenceField(); $layout = $event->getUpdatedLayout()->getParagraphsReferenceField(); return $original->isEmpty() != $layout->isEmpty(); } /** * Compares count == cardinality of the original and updated layouts. * * @param \Drupal\layout_paragraphs\Event\LayoutParagraphsUpdateLayoutEvent $event * The event. * * @return bool * True if the count == cardinality limit has changed. */ public function compareMaximumReached(LayoutParagraphsUpdateLayoutEvent $event) { $original_count = $event->getOriginalLayout()->getParagraphsReferenceField()->count(); $updated_count = $event->getUpdatedLayout()->getParagraphsReferenceField()->count(); $cardinality = $event->getUpdatedLayout() ->getParagraphsReferenceField() ->getFieldDefinition() ->getFieldStorageDefinition() ->getCardinality(); $original_is_max = $cardinality > 0 && $cardinality <= $original_count; $updated_is_max = $cardinality > 0 && $cardinality <= $updated_count; $event->needsRefresh = ($original->isEmpty() != $layout->isEmpty()); return $original_is_max != $updated_is_max; } }
tests/src/FunctionalJavascript/CardinalityTest.php 0 → 100644 +70 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\layout_paragraphs\FunctionalJavascript; /** * Tests cardinality settings for a Layout Paragraphs field widget. * * @group layout_paragraphs */ class CardinalityTest extends BuilderTestBase { /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->loginWithPermissions([ 'administer site configuration', 'administer node fields', 'administer node display', 'administer paragraphs types', ]); $this->drupalGet('admin/structure/types/manage/page/fields/node.page.field_content/storage'); $this->submitForm([ 'cardinality' => 'number', 'cardinality_number' => 2, ], 'Save field settings'); } /** * Tests cardinality settings for a Layout Paragraphs field widget. */ public function testCardinality() { $this->loginWithPermissions([ 'create page content', 'edit own page content', ]); $this->drupalGet('node/add/page'); $page = $this->getSession()->getPage(); // Add a three-column section. $this->addSectionComponent(2, '.lpb-btn--add'); // Cardinality is set to 2. We should still have (+) buttons. $this->assertSession()->elementExists('css', '.layout__region--first .lpb-btn--add'); $this->htmlOutput($this->getSession()->getPage()->getHtml()); // Add a text component. $this->addTextComponent('Some arbitrary text', '.layout__region--first .lpb-btn--add'); // Maximum number has been reached. There should be no more (+) buttons. $this->assertSession()->elementNotExists('css', '.layout__region--first .lpb-btn--add'); $this->htmlOutput($this->getSession()->getPage()->getHtml()); // Remove a component. $button = $page->find('css', '.layout__region--first a.lpb-delete'); $button->click(); $this->assertSession()->assertWaitOnAjaxRequest(); $button = $page->find('css', 'button.lpb-btn--confirm-delete'); $button->click(); $this->assertSession()->assertWaitOnAjaxRequest(); // We no longer have the maximum allowed items, and should have (+) buttons. $this->assertSession()->elementExists('css', '.layout__region--first .lpb-btn--add'); $this->htmlOutput($this->getSession()->getPage()->getHtml()); } }