diff --git a/src/Form/AjaxFormTrait.php b/src/Form/AjaxFormTrait.php new file mode 100644 index 0000000..0bc4bc7 --- /dev/null +++ b/src/Form/AjaxFormTrait.php @@ -0,0 +1,48 @@ + ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 'auto', + ]), + ]; + } + + /** + * Gets attributes for use with an add button AJAX modal. + * + * @return array + */ + public static function getAjaxButtonAttributes() { + return NestedArray::mergeDeep(AjaxFormTrait::getAjaxAttributes(), [ + 'class' => [ + 'button', + 'button--small', + 'button-action', + ], + ]); + } + +} diff --git a/src/Plugin/BlockPluginCollection.php b/src/Plugin/BlockPluginCollection.php new file mode 100644 index 0000000..1d3dd0f --- /dev/null +++ b/src/Plugin/BlockPluginCollection.php @@ -0,0 +1,58 @@ + $block) { + $configuration = $block->getConfiguration(); + $region = isset($configuration['region']) ? $configuration['region'] : NULL; + $region_assignments[$region][$block_id] = $block; + } + foreach ($region_assignments as $region => $region_assignment) { + // @todo Determine the reason this needs error suppression. + @uasort($region_assignment, function (BlockPluginInterface $a, BlockPluginInterface $b) { + $a_config = $a->getConfiguration(); + $a_weight = isset($a_config['weight']) ? $a_config['weight'] : 0; + $b_config = $b->getConfiguration(); + $b_weight = isset($b_config['weight']) ? $b_config['weight'] : 0; + if ($a_weight == $b_weight) { + return strcmp($a->label(), $b->label()); + } + return $a_weight > $b_weight ? 1 : -1; + }); + $region_assignments[$region] = $region_assignment; + } + return $region_assignments; + } + +} diff --git a/src/Plugin/BlockVariantInterface.php b/src/Plugin/BlockVariantInterface.php new file mode 100644 index 0000000..bb745ee --- /dev/null +++ b/src/Plugin/BlockVariantInterface.php @@ -0,0 +1,101 @@ +getBlockCollection()->get($block_id); + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::addBlock() + */ + public function addBlock(array $configuration) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + $this->getBlockCollection()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::removeBlock() + */ + public function removeBlock($block_id) { + $this->getBlockCollection()->removeInstanceId($block_id); + return $this; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::updateBlock() + */ + public function updateBlock($block_id, array $configuration) { + $existing_configuration = $this->getBlock($block_id)->getConfiguration(); + $this->getBlockCollection()->setInstanceConfiguration($block_id, $configuration + $existing_configuration); + return $this; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignment() + */ + public function getRegionAssignment($block_id) { + $configuration = $this->getBlock($block_id)->getConfiguration(); + return isset($configuration['region']) ? $configuration['region'] : NULL; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignments() + */ + public function getRegionAssignments() { + // Build an array of the region names in the right order. + $empty = array_fill_keys(array_keys($this->getRegionNames()), []); + $full = $this->getBlockCollection()->getAllByRegion(); + // Merge it with the actual values to maintain the ordering. + return array_intersect_key(array_merge($empty, $full), $empty); + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionName() + */ + public function getRegionName($region) { + $regions = $this->getRegionNames(); + return isset($regions[$region]) ? $regions[$region] : ''; + } + + /** + * Returns the block plugins used for this display variant. + * + * @return \Drupal\Core\Block\BlockPluginInterface[]|\Drupal\ctools\Plugin\BlockPluginCollection + * An array or collection of configured block plugins. + */ + protected function getBlockCollection() { + if (!$this->blockPluginCollection) { + $this->blockPluginCollection = new BlockPluginCollection(\Drupal::service('plugin.manager.block'), $this->getBlockConfig()); + } + return $this->blockPluginCollection; + } + + /** + * Returns the UUID generator. + * + * @return \Drupal\Component\Uuid\UuidInterface + */ + abstract protected function uuidGenerator(); + + /** + * Returns the configuration for stored blocks. + * + * @return array + * An array of block configuration, keyed by the unique block ID. + */ + abstract protected function getBlockConfig(); + +} diff --git a/src/Plugin/ConditionVariantInterface.php b/src/Plugin/ConditionVariantInterface.php new file mode 100644 index 0000000..242aecc --- /dev/null +++ b/src/Plugin/ConditionVariantInterface.php @@ -0,0 +1,92 @@ +selectionConditionCollection) { + $this->selectionConditionCollection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $this->getSelectionConfiguration()); + } + return $this->selectionConditionCollection; + } + + /** + * @see \Drupal\ctools\Plugin\ConditionVariantInterface::addSelectionCondition() + */ + public function addSelectionCondition(array $configuration) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + $this->getSelectionConditions()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * @see \Drupal\ctools\Plugin\ConditionVariantInterface::getSelectionCondition() + */ + public function getSelectionCondition($condition_id) { + return $this->getSelectionConditions()->get($condition_id); + } + + /** + * @see \Drupal\ctools\Plugin\ConditionVariantInterface::removeSelectionCondition() + */ + public function removeSelectionCondition($condition_id) { + $this->getSelectionConditions()->removeInstanceId($condition_id); + return $this; + } + + /** + * Determines if the selection conditions will pass given a set of contexts. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of set contexts, keyed by context name. + * + * @return bool + * TRUE if access is granted, FALSE otherwise. + */ + protected function determineSelectionAccess(array $contexts) { + $conditions = $this->getSelectionConditions(); + foreach ($conditions as $condition) { + if ($condition instanceof ContextAwarePluginInterface) { + $this->contextHandler()->applyContextMapping($condition, $contexts); + } + } + return $this->resolveConditions($conditions, $this->getSelectionLogic()); + } + + /** + * @see \Drupal\ctools\Plugin\ConditionVariantInterface::getSelectionLogic() + */ + abstract public function getSelectionLogic(); + + /** + * Returns the configuration for stored selection conditions. + * + * @return array + * An array of condition configuration, keyed by the unique condition ID. + */ + abstract protected function getSelectionConfiguration(); + + /** + * Returns the UUID generator. + * + * @return \Drupal\Component\Uuid\UuidInterface + */ + abstract protected function uuidGenerator(); + + /** + * Returns the context handler. + * + * @return \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + abstract protected function contextHandler(); + +} diff --git a/src/Plugin/DisplayVariant/BlockDisplayVariant.php b/src/Plugin/DisplayVariant/BlockDisplayVariant.php new file mode 100644 index 0000000..cf83b56 --- /dev/null +++ b/src/Plugin/DisplayVariant/BlockDisplayVariant.php @@ -0,0 +1,215 @@ +contextHandler = $context_handler; + $this->account = $account; + $this->uuidGenerator = $uuid_generator; + $this->token = $token; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('context.handler'), + $container->get('current_user'), + $container->get('uuid'), + $container->get('token') + ); + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account = NULL) { + // Delegate to the conditions. + return $this->determineSelectionAccess($this->getContexts()); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return parent::defaultConfiguration() + [ + 'blocks' => [], + 'selection_conditions' => [], + 'selection_logic' => 'and', + ]; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + foreach ($this->getBlockCollection() as $instance) { + $this->calculatePluginDependencies($instance); + } + foreach ($this->getSelectionConditions() as $instance) { + $this->calculatePluginDependencies($instance); + } + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return [ + 'selection_conditions' => $this->getSelectionConditions()->getConfiguration(), + 'blocks' => $this->getBlockCollection()->getConfiguration(), + ] + parent::getConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function getSelectionLogic() { + return $this->configuration['selection_logic']; + } + + /** + * Gets the contexts. + * + * @return \Drupal\Component\Plugin\Context\ContextInterface[] + * An array of set contexts, keyed by context name. + */ + public function getContexts() { + return $this->contexts; + } + + /** + * Sets the contexts. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of contexts, keyed by context name. + * + * @return $this + */ + public function setContexts(array $contexts) { + $this->contexts = $contexts; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function contextHandler() { + return $this->contextHandler; + } + + /** + * {@inheritdoc} + */ + protected function getSelectionConfiguration() { + return $this->configuration['selection_conditions']; + } + + /** + * {@inheritdoc} + */ + protected function getBlockConfig() { + return $this->configuration['blocks']; + } + + /** + * {@inheritdoc} + */ + protected function uuidGenerator() { + return $this->uuidGenerator; + } + +} diff --git a/tests/src/Unit/BlockDisplayVariantTest.php b/tests/src/Unit/BlockDisplayVariantTest.php new file mode 100644 index 0000000..e9384b9 --- /dev/null +++ b/tests/src/Unit/BlockDisplayVariantTest.php @@ -0,0 +1,116 @@ +getMockBuilder(TestBlockDisplayVariant::class) + ->disableOriginalConstructor() + ->setMethods(['determineSelectionAccess']) + ->getMock(); + $display_variant->expects($this->once()) + ->method('determineSelectionAccess') + ->willReturn(FALSE); + $this->assertSame(FALSE, $display_variant->access()); + + $display_variant = $this->getMockBuilder(TestBlockDisplayVariant::class) + ->disableOriginalConstructor() + ->setMethods(['determineSelectionAccess']) + ->getMock(); + $display_variant->expects($this->once()) + ->method('determineSelectionAccess') + ->willReturn(TRUE); + $this->assertSame(TRUE, $display_variant->access()); + } + + /** + * Tests the submitConfigurationForm() method. + * + * @covers ::submitConfigurationForm + * + * @dataProvider providerTestSubmitConfigurationForm + */ + public function testSubmitConfigurationForm($values) { + $account = $this->prophesize(AccountInterface::class); + $context_handler = $this->prophesize(ContextHandlerInterface::class); + $uuid_generator = $this->prophesize(UuidInterface::class); + $token = $this->prophesize(Token::class); + + $display_variant = new TestBlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal()); + + $form = []; + $form_state = (new FormState())->setValues($values); + $display_variant->submitConfigurationForm($form, $form_state); + $this->assertSame($values['label'], $display_variant->label()); + } + + /** + * Provides data for testSubmitConfigurationForm(). + */ + public function providerTestSubmitConfigurationForm() { + $data = []; + $data[] = [ + [ + 'label' => 'test_label1', + ], + ]; + $data[] = [ + [ + 'label' => 'test_label2', + 'blocks' => ['foo1' => []], + ], + ]; + $data[] = [ + [ + 'label' => 'test_label3', + 'blocks' => ['foo1' => [], 'foo2' => []], + ], + ]; + return $data; + } + +} + +class TestBlockDisplayVariant extends BlockDisplayVariant { + + /** + * {@inheritdoc} + */ + public function build() { + return []; + } + + public function getRegionNames() { + return [ + 'top' => 'Top', + 'bottom' => 'Bottom', + ]; + } + +} diff --git a/tests/src/Unit/BlockPluginCollectionTest.php b/tests/src/Unit/BlockPluginCollectionTest.php new file mode 100644 index 0000000..2aee6a4 --- /dev/null +++ b/tests/src/Unit/BlockPluginCollectionTest.php @@ -0,0 +1,86 @@ + [ + 'id' => 'foo', + 'label' => 'Foo', + 'plugin' => 'system_powered_by_block', + 'region' => 'bottom', + ], + 'bar' => [ + 'id' => 'bar', + 'label' => 'Bar', + 'plugin' => 'system_powered_by_block', + 'region' => 'top', + ], + 'bing' => [ + 'id' => 'bing', + 'label' => 'Bing', + 'plugin' => 'system_powered_by_block', + 'region' => 'bottom', + 'weight' => -10, + ], + 'baz' => [ + 'id' => 'baz', + 'label' => 'Baz', + 'plugin' => 'system_powered_by_block', + 'region' => 'bottom', + ], + ]; + $block_manager = $this->prophesize(BlockManagerInterface::class); + $plugins = []; + foreach ($blocks as $block_id => $block) { + $plugin = $this->prophesize(BlockPluginInterface::class); + $plugin->label()->willReturn($block['label']); + $plugin->getConfiguration()->willReturn($block); + $plugins[$block_id] = $plugin->reveal(); + + $block_manager->createInstance($block_id, $block) + ->willReturn($plugin->reveal()) + ->shouldBeCalled(); + } + + + $block_plugin_collection = new BlockPluginCollection($block_manager->reveal(), $blocks); + $expected = [ + 'bottom' => [ + 'bing' => $plugins['bing'], + 'baz' => $plugins['baz'], + 'foo' => $plugins['foo'], + ], + 'top' => [ + 'bar' => $plugins['bar'], + ], + ]; + $this->assertSame($expected, $block_plugin_collection->getAllByRegion()); + } + +} diff --git a/tests/src/Unit/BlockVariantTraitTest.php b/tests/src/Unit/BlockVariantTraitTest.php new file mode 100644 index 0000000..a56cfe5 --- /dev/null +++ b/tests/src/Unit/BlockVariantTraitTest.php @@ -0,0 +1,154 @@ +prophesize(BlockPluginCollection::class); + $block_collection->getAllByRegion() + ->willReturn($blocks) + ->shouldBeCalled(); + + $display_variant = new TestBlockVariantTrait(); + $display_variant->setBlockPluginCollection($block_collection->reveal()); + + $this->assertSame($expected, $display_variant->getRegionAssignments()); + } + + public function providerTestGetRegionAssignments() { + return [ + [ + [ + 'top' => [], + 'bottom' => [], + ], + ], + [ + [ + 'top' => ['foo'], + 'bottom' => [], + ], + [ + 'top' => ['foo'], + ], + ], + [ + [ + 'top' => [], + 'bottom' => [], + ], + [ + 'invalid' => ['foo'], + ], + ], + [ + [ + 'top' => [], + 'bottom' => ['foo'], + ], + [ + 'bottom' => ['foo'], + 'invalid' => ['bar'], + ], + ], + ]; + } + +} + +class TestBlockVariantTrait { + use BlockVariantTrait; + + /** + * @var array + */ + protected $blockConfig = []; + + /** + * @var \Drupal\Component\Uuid\UuidInterface + */ + protected $uuidGenerator; + + /** + * @param BlockPluginCollection $block_plugin_collection + * + * @return $this + */ + public function setBlockPluginCollection(BlockPluginCollection $block_plugin_collection) { + $this->blockPluginCollection = $block_plugin_collection; + return $this; + } + + /** + * @param \Drupal\Component\Uuid\UuidInterface $uuid_generator + * + * @return $this + */ + public function setUuidGenerator(UuidInterface $uuid_generator) { + $this->uuidGenerator = $uuid_generator; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function uuidGenerator() { + return $this->uuidGenerator; + } + + /** + * Sets the block configuration. + * + * @param array $config + * The block configuration. + * + * @return $this + */ + public function setBlockConfig(array $config) { + $this->blockConfig = $config; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function getBlockConfig() { + return $this->blockConfig; + } + + /** + * {@inheritdoc} + */ + public function getRegionNames() { + return [ + 'top' => 'Top', + 'bottom' => 'Bottom', + ]; + } + +}