From 51cdafbefac71d2a46be740b31581f6117c27aee Mon Sep 17 00:00:00 2001 From: "Christian.wiedemann" <7688-Christian.wiedemann@users.noreply.drupalcode.org> Date: Mon, 17 Jun 2024 19:48:21 +0000 Subject: [PATCH] Issue #3455074 by Christian.wiedemann: [2.0.0-alpha3] Refactoring of form elements --- src/Element/ComponentForm.php | 68 ++--- src/Element/ComponentFormBase.php | 20 -- src/Element/ComponentPropForm.php | 210 ++++++++++++++++ src/Element/ComponentPropsForm.php | 184 +++----------- src/Element/ComponentSlotForm.php | 383 +++++++++++++++++++++++++++++ src/Element/ComponentSlotsForm.php | 331 ++----------------------- 6 files changed, 689 insertions(+), 507 deletions(-) create mode 100644 src/Element/ComponentPropForm.php create mode 100644 src/Element/ComponentSlotForm.php diff --git a/src/Element/ComponentForm.php b/src/Element/ComponentForm.php index fb04e0cc1..2624babde 100644 --- a/src/Element/ComponentForm.php +++ b/src/Element/ComponentForm.php @@ -4,44 +4,48 @@ namespace Drupal\ui_patterns\Element; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; /** * Provides a Component form builder element. * - * Use #component_id to set an unchangeable component. - * - * The value of the form element contains following keys: - * [ - * 'component_id' => 'my_module:my_component', - * 'variant_id' => 'variant', - * 'slots' => [ - * 'slots_id' => [ - * ['source_id' => 'id', 'value' => 'Source value'] - * ], - * ], - * 'props' => [ - * ['props_id' => - * ['source_id' => 'id', 'value' => 'Source value '] - * ] - * ], - * ] - * The same component value is returned in the form element. - * * Usage example: * * @code * $form['component_form'] = [ * '#type' => 'component_form', - * * '#default_value' => [ - * 'component_id' => 'my_module:my_component', - * 'variant_id' => 'variant', - * 'slots' => [], - * 'props' => [], * ], * ]; * @endcode * + * Value example: + * + * @code + * ['#default_value' => [ + * 'component_id' => 'my_module:my_component', + * 'variant_id' => 'variant', + * 'slots' => [ + * 'slots_id' => [ + * ['source_id' => 'id', 'value' => 'Source value'] + * ], + * ], + * 'props' => [ + * ['props_id' => + * ['source_id' => 'id', 'value' => 'Source value '] + * ] + * ], + * ] + * ] + * @endcode + * + * Additional Configuration: + * + * '#component_id' => Optional Component Id. If not set a component selector is set. + * '#source_contexts' => The context of the sources. + * '#tag_filter' => Filter sources based on this tags. + * * @FormElement("component_form") */ class ComponentForm extends ComponentFormBase { @@ -58,6 +62,7 @@ class ComponentForm extends ComponentFormBase { '#multiple' => FALSE, '#default_value' => NULL, '#source_contexts' => [], + '#tag_filter' => [], '#process' => [ [$class, 'buildForm'], ], @@ -96,24 +101,21 @@ class ComponentForm extends ComponentFormBase { * Processes the main form element including component selector. */ public static function buildForm(array &$element, FormStateInterface $form_state) { - $trigger_element = $form_state->getTriggeringElement(); - if ($form_state->isRebuilding() && isset($trigger_element['#ui_patterns'])) { - $parents_for_trigger = self::getComponentFormStateParents($trigger_element['#parents']); - if ($parents_for_trigger == $element['#parents']) { - $value = $form_state->getValue($parents_for_trigger); - self::valueCallback($element, $value, $form_state); - } - } $initial_component_id = $element['#component_id'] ?? NULL; $component_id = $initial_component_id ?? $element['#default_value']['component_id'] ?? NULL; $wrapper_id = static::getElementId($element, 'ui-patterns-component'); + if ($component_id) { + $contextComponentDefinition = ContextDefinition::create('string'); + $element['#source_contexts']['component_id'] = new Context($contextComponentDefinition, $component_id); + } if ($initial_component_id === NULL) { $element["component_id"] = self::expandAjax(self::buildComponentSelectorForm( $wrapper_id, $component_id )); } + self::buildComponentForm( $element, $wrapper_id, @@ -221,6 +223,7 @@ class ComponentForm extends ComponentFormBase { '#type' => 'component_slots_form', '#component_id' => $component_id, '#source_contexts' => $element['#source_contexts'], + '#tag_filter' =>$element['#tag_filter'], '#ajax_url' => $element['#ajax_url'], '#access' => $element['#render_slots'] ?? TRUE, '#default_value' => [ @@ -238,6 +241,7 @@ class ComponentForm extends ComponentFormBase { '#type' => 'component_props_form', '#component_id' => $component_id, '#source_contexts' => $element['#source_contexts'], + '#tag_filter' =>$element['#tag_filter'], '#ajax_url' => $element['#ajax_url'], '#access' => $element['#render_props'] ?? TRUE, '#default_value' => [ diff --git a/src/Element/ComponentFormBase.php b/src/Element/ComponentFormBase.php index 052d66243..d40ee0e29 100644 --- a/src/Element/ComponentFormBase.php +++ b/src/Element/ComponentFormBase.php @@ -50,24 +50,4 @@ abstract class ComponentFormBase extends FormElementBase { return $component_id ? $component_plugin_manager->find($component_id) : NULL; } - /** - * Returns the parents belonging to the form builder. - * - * @param array $parents - * The form_state '#parents' value. - * - * @return array - * The parents to locate the form builder. - */ - protected static function getComponentFormStateParents(array $parents): array { - $reverse_parents = array_reverse($parents); - $needle = array_search('ui_patterns', $reverse_parents); - if (FALSE === $needle) { - return []; - } - $sliced = count($parents) - $needle; - $returned = array_slice($parents, 0, $sliced); - return $returned; - } - } diff --git a/src/Element/ComponentPropForm.php b/src/Element/ComponentPropForm.php new file mode 100644 index 000000000..b119af138 --- /dev/null +++ b/src/Element/ComponentPropForm.php @@ -0,0 +1,210 @@ +<?php + +namespace Drupal\ui_patterns\Element; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; +use Drupal\ui_patterns\SourceInterface; +use Drupal\ui_patterns\SourcePluginBase; + +/** + * Component to render a single prop. + * + * Usage example: + * + * @code + * $form['prop_name'] = [ + * '#type' => 'component_prop_form', + * '#component_id' => 'component_id', + * '#prop_id' => 'prop' + * '#default_value' => [ + * 'source' => [], + * 'source_id' => 'textfield' + * ], + * ]; + * @endcode + * + * Value example: + * + * @code + * '#default_value' => ['source_id' => 'id', 'source' => []] + * @endcode + * + * Configuration: + * + * '#component_id' => Required Component ID. + * '#prop_id' => Required Prop ID. + * '#source_contexts' => The context of the sources. + * '#tag_filter' => Filter sources based on these tags. + * + * @FormElement("component_prop_form") + */ +class ComponentPropForm extends ComponentFormBase { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return [ + '#input' => TRUE, + '#multiple' => FALSE, + '#default_value' => NULL, + '#source_contexts' => [], + '#tag_filter' => [], + '#component_id' => NULL, + '#slot_id' => NULL, + '#process' => [ + [$class, 'buildForm'], + ], + '#theme_wrappers' => ['fieldset'], + ]; + } + + /** + * Build props forms. + */ + public static function buildForm(array &$element, FormStateInterface $form_state): array { + $element['#tree'] = TRUE; + $prop_id = $element['#prop_id']; + $component = static::getComponent($element); + $props = $component->metadata->schema['properties']; + $definition = $props[$prop_id]; + $configuration = $element['#default_value'] ?? []; + $sources = static::getSources($prop_id, $definition, $element); + $selected_source = static::getSelectedSource($configuration, $sources); + if (!$selected_source) { + $selected_source = static::getDefaultSource($prop_id, $definition, $element); + if (!isset($sources[$selected_source->getPluginId()])) { + $selected_source = current($sources); + } + } + if (!$selected_source) { + return []; + } + + $wrapper_id = static::getElementId($element, 'ui-patterns-prop-item-' . $prop_id); + $source_selector = static::buildSourceSelector($sources, $selected_source, $wrapper_id); + $source_form = static::getSourcePluginForm($form_state, $selected_source, $wrapper_id); + + $element += [ + 'source_id' => $source_selector, + 'source' => $source_form, + ]; + $element['#attributes']['style'] = 'position: relative;'; + return $element; + } + + /** + * Get source plugin form. + */ + protected static function getSourcePluginForm(FormStateInterface $form_state, SourceInterface $source, string $wrapper_id): array { + $form = $source->settingsForm([], $form_state); + $form["#type"] = 'container'; + $form['#attributes'] = [ + 'id' => $wrapper_id, + ]; + // Weird, but :switchSourceForm() AJAX handler doesn't work without that. + foreach (Element::children($form) as $child) { + if (isset($form[$child]['#description']) && !isset($form[$child]['#description_display'])) { + $form[$child]['#description_display'] = 'after'; + } + } + return $form; + } + + /** + * Build sources selector widget. + */ + protected static function buildSourceSelector(array $sources, SourceInterface $selected_source, string $wrapper_id): array { + if (empty($sources)) { + return []; + } + if (count($sources) == 1) { + return [ + '#type' => 'hidden', + '#value' => array_keys($sources)[0], + ]; + } + $options = []; + foreach ($sources as $source_id => $source) { + $options[$source_id] = $source->label(); + } + return [ + '#type' => 'select', + "#options" => $options, + '#default_value' => $selected_source->getPluginId(), + '#attributes' => [ + 'style' => "position: absolute; top: 0; right: 0;", + ], + '#prop_id' => $selected_source->getPropId(), + '#prop_definition' => $selected_source->getPropDefinition(), + '#ajax' => [ + 'callback' => [ + static::class, + 'switchSourceForm', + ], + 'wrapper' => $wrapper_id, + 'effect' => 'fade', + ], + ]; + } + + /** + * Ajax handler: Switch source plugin form. + */ + public static function switchSourceForm(array $form, FormStateInterface $form_state): array { + $selector = $form_state->getTriggeringElement(); + $parents = $selector["#array_parents"]; + $subform = NestedArray::getValue($form, array_slice($parents, 0, -1)); + return $subform["source"]; + } + + /** + * Get sources for a prop type. + */ + protected static function getSources(string $prop_id, array $definition, array $element): array { + $configuration = $element['#default_value'] ?? []; + $source_contexts = $element['#source_contexts']; + $form_array_parents = $element['#array_parents']; + $tag_filter = $element['#tag_filter']; + $prop_type = $definition['ui_patterns']['type_definition']; + $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source"); + $source_ids = array_keys($source_plugin_manager->getDefinitionsForPropType($prop_type->getPluginId(), $source_contexts, $tag_filter)); + $source_ids = array_combine($source_ids, $source_ids); + if (empty($source_ids)) { + return []; + } + return $source_plugin_manager->createInstances($source_ids, SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents)); + } + + /** + * Get selected source plugin. + */ + protected static function getSelectedSource(array $configuration, array $sources): ?SourceInterface { + if (isset($configuration['source_id']) && $sources[$configuration['source_id']]) { + return $sources[$configuration['source_id']]; + } + return NULL; + } + + /** + * Get default source plugin. + */ + protected static function getDefaultSource(string $prop_id, array $definition, $element): ?SourceInterface { + $configuration = $element['#default_value'] ?? []; + $source_contexts = $element['#source_contexts']; + $form_array_parents = $element['#array_parents']; + $tag_filter = $element['#tag_filter']; + $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source"); + $prop_type = $definition['ui_patterns']['type_definition']; + $source_id = $source_plugin_manager->getPropTypeDefault($prop_type->getPluginId(), $source_contexts, $tag_filter); + if (!$source_id) { + return NULL; + } + $plugin_configuration = SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents); + return $source_plugin_manager->createInstance($source_id, $plugin_configuration); + } + +} diff --git a/src/Element/ComponentPropsForm.php b/src/Element/ComponentPropsForm.php index 4186712d4..61851c7c1 100644 --- a/src/Element/ComponentPropsForm.php +++ b/src/Element/ComponentPropsForm.php @@ -2,29 +2,17 @@ namespace Drupal\ui_patterns\Element; -use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; -use Drupal\ui_patterns\SourceInterface; -use Drupal\ui_patterns\SourcePluginBase; /** - * Provides a Component Prop form builder element. - * - * The value of the form element contains following keys: - * [ - * 'props' => [ - * ['props_id' => - * ['source_id' => 'id', 'value' => 'Source value '] - * ] - * ], - * ] - * The same component value is returned in the form element. + * Component to render all props of a component. * * Usage example: * * @code - * $form['slots'] = [ + * $form['props'] = [ + * '#component_id' => 'component_id', * '#type' => 'component_props_form', * '#default_value' => [ * 'props' => [], @@ -32,6 +20,24 @@ use Drupal\ui_patterns\SourcePluginBase; * ]; * @endcode * + * Value example: + * + * @code + * ['#default_value' => + * 'props' => [ + * ['props_id' => + * ['source_id' => 'id', 'source' => []] + * ] + * ], + * ] + * @endcode + * + * Configuration: + * + * '#component_id' => Required Component ID. + * '#source_contexts' => The context of the sources. + * '#tag_filter' => Filter sources based on these tags. + * * @FormElement("component_props_form") */ class ComponentPropsForm extends ComponentFormBase { @@ -45,7 +51,9 @@ class ComponentPropsForm extends ComponentFormBase { '#input' => TRUE, '#multiple' => FALSE, '#default_value' => NULL, + '#component_id' => NULL, '#source_contexts' => [], + '#tag_filter' => [], '#process' => [ [$class, 'buildForm'], ], @@ -65,7 +73,16 @@ class ComponentPropsForm extends ComponentFormBase { } $configuration = $element['#default_value']['props'] ?? []; foreach ($props as $prop_id => $prop) { - $element[$prop_id] = static::buildPropForm($element, $form_state, $prop_id, $prop, $configuration[$prop_id] ?? [], $contexts); + $prop_type = $prop['ui_patterns']['type_definition']; + $element[$prop_id] = [ + '#type' => 'component_prop_form', + '#title' => $prop["title"] ?? $prop_type->label(), + '#default_value' => $configuration[$prop_id] ?? [], + '#source_contexts' => $contexts, + '#tag_filter' => $element['#tag_filter'], + '#component_id' => $component->getPluginId(), + '#prop_id' => $prop_id, + ]; } if (count(Element::children($element)) === 0) { hide($element); @@ -73,139 +90,4 @@ class ComponentPropsForm extends ComponentFormBase { return $element; } - /** - * Get sources for a prop type. - */ - protected static function getSources(string $prop_id, array $definition, array $configuration, array $source_contexts, array $form_array_parents): array { - $prop_type = $definition['ui_patterns']['type_definition']; - $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source"); - $source_ids = array_keys($source_plugin_manager->getDefinitionsForPropType($prop_type->getPluginId(), $source_contexts)); - $source_ids = array_combine($source_ids, $source_ids); - if (empty($source_ids)) { - return []; - } - return $source_plugin_manager->createInstances($source_ids, SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents)); - } - - /** - * Get selected source plugin. - */ - protected static function getSelectedSource(array $configuration, array $sources): ?SourceInterface { - if (isset($configuration['source_id']) && $sources[$configuration['source_id']]) { - return $sources[$configuration['source_id']]; - } - return NULL; - } - - /** - * Get default source plugin. - */ - protected static function getDefaultSource(string $prop_id, array $definition, array $configuration, array $source_contexts, array $form_array_parents): ?SourceInterface { - $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source"); - $prop_type = $definition['ui_patterns']['type_definition']; - $source_id = $source_plugin_manager->getPropTypeDefault($prop_type->getPluginId(), $source_contexts); - if (!$source_id) { - return NULL; - } - $plugin_configuration = SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents); - return $source_plugin_manager->createInstance($source_id, $plugin_configuration); - } - - /** - * Build single prop form. - */ - protected static function buildPropForm(array $element, FormStateInterface $form_state, string $prop_id, array $definition, array $configuration, array $source_contexts): array { - $form_array_parents = $element["#array_parents"]; - $sources = static::getSources($prop_id, $definition, $configuration, $source_contexts, $form_array_parents); - $selected_source = static::getSelectedSource($configuration, $sources); - if (!$selected_source) { - $selected_source = static::getDefaultSource($prop_id, $definition, $configuration, $source_contexts, $form_array_parents); - } - if (!$selected_source) { - return []; - } - - $form_array_parents[] = $prop_id; - $form_array_parents[] = "source"; - $wrapper_id = static::getElementId($element, 'ui-patterns-prop-item-' . $prop_id); - $source_selector = static::buildSourceSelector($sources, $selected_source, $wrapper_id); - $source_form = static::getSourcePluginForm($form_state, $selected_source, $wrapper_id, $form_array_parents); - $prop_type = $definition['ui_patterns']['type_definition']; - $build = [ - '#type' => 'fieldset', - '#title' => $definition["title"] ?? $prop_type->label(), - '#attributes' => [ - 'style' => "position: relative;", - ], - 'source_id' => $source_selector, - 'source' => $source_form, - ]; - return $build; - } - - /** - * Get source plugin form. - */ - protected static function getSourcePluginForm(FormStateInterface $form_state, SourceInterface $source, string $wrapper_id, array $form_array_parents): array { - $form = $source->settingsForm([], $form_state); - $form["#type"] = 'container'; - $form['#attributes'] = [ - 'id' => $wrapper_id, - ]; - // Weird, but :switchSourceForm() AJAX handler doesn't work without that. - foreach (Element::children($form) as $child) { - if (isset($form[$child]['#description']) && !isset($form[$child]['#description_display'])) { - $form[$child]['#description_display'] = 'after'; - } - } - return $form; - } - - /** - * Build sources selector widget. - */ - protected static function buildSourceSelector(array $sources, SourceInterface $selected_source, string $wrapper_id): array { - if (empty($sources)) { - return []; - } - if (count($sources) == 1) { - return [ - '#type' => 'hidden', - '#value' => array_keys($sources)[0], - ]; - } - $options = []; - foreach ($sources as $source_id => $source) { - $options[$source_id] = $source->label(); - } - return [ - '#type' => 'select', - "#options" => $options, - '#default_value' => $selected_source->getPluginId(), - '#attributes' => [ - 'style' => "position: absolute; top: 0; right: 0;", - ], - '#prop_id' => $selected_source->getPropId(), - '#prop_definition' => $selected_source->getPropDefinition(), - '#ajax' => [ - 'callback' => [ - static::class, - 'switchSourceForm', - ], - 'wrapper' => $wrapper_id, - 'effect' => 'fade', - ], - ]; - } - - /** - * Ajax handler: Switch source plugin form. - */ - public static function switchSourceForm(array $form, FormStateInterface $form_state): array { - $selector = $form_state->getTriggeringElement(); - $parents = $selector["#array_parents"]; - $subform = NestedArray::getValue($form, array_slice($parents, 0, -1)); - return $subform["source"]; - } - } diff --git a/src/Element/ComponentSlotForm.php b/src/Element/ComponentSlotForm.php new file mode 100644 index 000000000..ee23efe7f --- /dev/null +++ b/src/Element/ComponentSlotForm.php @@ -0,0 +1,383 @@ +<?php + +namespace Drupal\ui_patterns\Element; + +use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; +use Drupal\ui_patterns\SourcePluginBase; + +/** + * Component to render a single slot. + * + * Usage example: + * + * @code + * $form['slot'] = [ + * '#type' => 'component_slot_form', + * '#component_id' => 'card', + * '#slot_id' => 'body', + * '#default_value' => [ + * 'sources' => [], + * ], + * ]; + * @endcode + * + * Value example: + * + * @code + * ['#default_value' => + * ['sources' => + * ['source_id' => 'id', 'value' => []] + * ] + * ] + * @endcode + * + * Configuration: + * + * '#component_id' => Optional Component ID. A slot can rendered without knowing any context. + * '#slot_id' => Optional Slot ID. + * '#source_contexts' => The context of the sources. + * '#tag_filter' => Filter sources based on these tags. + * '#display_remove' => Display or hide the remove button. Default = true + * '#cardinality_multiple' => Allow or disallow multiple slot items + * + * @FormElement("component_slot_form") + */ +class ComponentSlotForm extends ComponentFormBase { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return [ + '#input' => TRUE, + '#multiple' => FALSE, + '#default_value' => NULL, + '#source_contexts' => [], + '#tag_filter' => [], + '#display_remove' => TRUE, + '#component_id' => NULL, + '#slot_id' => NULL, + '#cardinality_multiple' => TRUE, + '#process' => [ + [$class, 'buildForm'], + ], + '#theme_wrappers' => ['fieldset'], + ]; + } + + /** + * Build single slot form. + */ + public static function buildForm(array &$element, FormStateInterface $form_state): array { + $slot_id = $element['#slot_id']; + $trigger_element = $form_state->getTriggeringElement(); + if ($form_state->isRebuilding() && isset($trigger_element['#ui_patterns_slot'])) { + if ($trigger_element['#ui_patterns_slot_parents'] == $element['#parents']) { + $value = $form_state->getValue($trigger_element['#ui_patterns_slot_parents']); + $element['#default_value'] = $value; + } + } + + $component = static::getComponent($element); + if ($component !== NULL) { + $slots = $component->metadata->slots; + $definition = $slots[$slot_id]; + } + else { + /** @var \Drupal\ui_patterns\PropTypePluginManager $prop_type_manager */ + $prop_type_manager = \Drupal::service("plugin.manager.ui_patterns_prop_type"); + $definition = [ + 'ui_patterns' => $prop_type_manager->createInstance('slot', []) + ]; + } + + $wrapper_id = static::getElementId($element, 'ui-patterns-slot-' . $slot_id); + $element['#prefix'] = '<div id="' . $wrapper_id . '">'; + $element['#suffix'] = '</div>'; + $element['#tree'] = TRUE; + + $element['sources'] = static::buildSourcesForm($element, $form_state, $definition, $wrapper_id); + if ($element['#cardinality_multiple'] === TRUE || + (!isset($element['#default_value']['sources']) || count($element['#default_value']['sources']) === 0)) { + $element['add_more_button'] = static::buildSourceSelector($element, $wrapper_id); + } + + return $element; + } + + /** + * Returns the dropdown options array. + */ + private static function getSourceOptions($element):array { + if (isset($element['#source_options'])) { + return $element['#source_options']; + } + /** @var \Drupal\ui_patterns\SourcePluginManager $sources_manager */ + $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source"); + $source_contexts = $element['#source_contexts'] ?? []; + $tag_filter = $element['#tag_filter'] ?? []; + $sources = $sources_manager->getDefinitionsForPropType('slot', $source_contexts, $tag_filter); + + $source_ids = array_keys($sources); + $source_ids = array_combine($source_ids, $source_ids); + + $valid_source_plugins = $sources_manager->createInstances( + $source_ids, + SourcePluginBase::buildConfiguration('slot', [], [], $source_contexts, $element['#array_parents']), + ); + $options = []; + foreach ($valid_source_plugins as $valid_source_plugin) { + $options[$valid_source_plugin->getPluginId()] = $valid_source_plugin->label(); + } + // Cache source options for performance reasons. + $element['#source_options'] = $options; + return $options; + } + + /** + * Build single slot's sources form. + */ + protected static function buildSourcesForm($element, $form_state, array $definition, string $wrapper_id): array { + $configuration = $element['#default_value'] ?? []; + $form = [ + '#theme' => 'field_multiple_value_form', + '#title' => $element['#title'] ?? '', + '#cardinality_multiple' => $element['#cardinality_multiple'], + ]; + // Add fake #field_name to avoid errors from + // template_preprocess_field_multiple_value_form. + $form['#field_name'] = "foo"; + if (!isset($configuration['sources'])) { + return $form; + } + foreach ($configuration['sources'] as $delta => $source_configuration) { + if (!isset($source_configuration['source_id'])) { + continue; + } + $form[$delta] = static::buildSourceForm($element, $form_state, $definition, $source_configuration, $delta, $wrapper_id); + } + return $form; + } + + /** + * Build single source form. + */ + protected static function buildSourceForm(array $element, FormStateInterface $form_state, array $definition, array $configuration, int $delta, string $wrapper_id): array { + $slot_id = $element['#slot_id'] ?? NULL; + $form_array_parents = $element["#array_parents"]; + $source_contexts = $element['#source_contexts'] ?? []; + $form_array_parents[] = $slot_id ?? 'default'; + $form_array_parents[] = $delta; + $form = []; + $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source"); + $source = $sources_manager->createInstance( + $configuration['source_id'], + SourcePluginBase::buildConfiguration($slot_id, $definition, $configuration, $source_contexts, $form_array_parents) + ); + $form['source'] = $source->settingsForm([], $form_state); + $form['source_id'] = [ + '#type' => 'hidden', + '#value' => $source->getPluginId(), + ]; + $form['_weight'] = [ + '#type' => 'weight', + '#title' => t( + 'Weight for row @number', + ['@number' => $delta + 1] + ), + '#title_display' => 'invisible', + '#delta' => count($form), + '#default_value' => $configuration['_weight'] ?? $delta, + '#weight' => 100, + ]; + if ($element['#display_remove'] === TRUE) { + $form['_remove'] = static::buildRemoveSourceButton($element, $slot_id, $wrapper_id, $delta); + } + + return $form; + } + + /** + * Build widget to remove source. + */ + protected static function buildRemoveSourceButton(array $element, string $slot_id, string $wrapper_id, int $delta): array { + $id = implode('-', $element['#array_parents']); + $remove_action = [ + '#type' => 'submit', + '#name' => strtr($slot_id, '-', '_') . $id . '_' . $delta . '_remove', + '#value' => t('Remove'), + '#submit' => [ + static::class . '::removeSource', + ], + '#access' => TRUE, + '#delta' => $delta, + '#ui_patterns_slot' => TRUE, + '#ui_patterns_slot_parents' => $element['#parents'], + '#ui_patterns_slot_array_parents' => $element['#array_parents'], + '#ajax' => [ + 'callback' => [static::class, 'refreshForm'], + 'wrapper' => $wrapper_id, + 'effect' => 'fade', + ], + ]; + return [ + '#type' => 'container', + 'dropdown_actions' => [ + static::expandComponentButton($element, $remove_action), + ], + ]; + } + + /** + * Build source selector. + */ + protected static function buildSourceSelector(array $element, string $wrapper_id): array { + $options = self::getSourceOptions($element); + $slot_id = $element['#slot_id']; + $action_buttons = []; + foreach ($options as $source_id => $source_label) { + $action_buttons[$source_id] = static::expandComponentButton($element, [ + '#type' => 'submit', + '#name' => strtr($slot_id, '-', '_') . implode('-', $element['#array_parents']) . '_' . $source_id . '_add_more', + '#value' => t('Add %source', ['%source' => $source_label]), + '#submit' => [ + static::class . '::addSource', + ], + '#access' => TRUE, + '#source_id' => $source_id, + '#ui_patterns_slot' => TRUE, + '#ui_patterns_slot_parents' => $element['#parents'], + '#ui_patterns_slot_array_parents' => $element['#array_parents'], + '#ajax' => [ + 'callback' => [ + static::class, + 'refreshForm', + ], + 'wrapper' => $wrapper_id, + 'effect' => 'fade', + ], + ]); + } + return static::buildComponentDropbutton($action_buttons); + } + + /** + * Build drop button. + * + * @param array $elements + * Elements for drop button. + * + * @return array + * Drop button array. + */ + protected static function buildComponentDropbutton(array $elements = []): array { + $build = [ + '#type' => 'container', + '#attributes' => ['class' => ['ui-patterns-dropbutton-wrapper']], + ]; + + $operations = []; + // Because we are cloning the elements into title sub element we need to + // sort children first. + foreach (Element::children($elements, TRUE) as $child) { + // Clone the element as an operation. + $operations[$child] = ['title' => $elements[$child]]; + + // Flag the original element as printed so it doesn't render twice. + $elements[$child]['#printed'] = TRUE; + } + + $build['operations'] = [ + '#type' => 'ui_patterns_operations', + // Even though operations are run through the "links" element type, the + // theme system will render any render array passed as a link "title". + '#links' => $operations, + '#dropbutton_type' => 'small', + ]; + + return $build + $elements; + } + + /** + * Expand button base array into a paragraph widget action button. + * + * @param array $element + * Element. + * @param array $button_base + * Button base render array. + * + * @return array + * Button render array. + */ + protected static function expandComponentButton(array $element, array $button_base): array { + // Do not expand elements that do not have submit handler. + if (empty($button_base['#submit'])) { + return $button_base; + } + + $button = $button_base + [ + '#type' => 'submit', + '#theme_wrappers' => ['input__submit__ui_patterns_action'], + ]; + + // Html::getId will give us '-' char in name but we want '_' for now so + // we use strtr to search&replace '-' to '_'. + $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_'); + $button['#id'] = static::getElementId($element, $button['#name']); + + if (isset($button['#ajax'])) { + $button['#ajax'] += [ + 'effect' => 'fade', + // Since a normal throbber is added inline, this has the potential to + // break a layout if the button is located in dropbuttons. Instead, + // it's safer to just show the fullscreen progress element instead. + 'progress' => ['type' => 'fullscreen'], + ]; + } + + return static::expandAjax($button); + } + + /** + * Ajax submit handler: Add source. + */ + public static function addSource(array $form, FormStateInterface $form_state) { + $trigger_element = $form_state->getTriggeringElement(); + $source_id = $trigger_element['#source_id']; + $component_form_parents = $trigger_element['#ui_patterns_slot_parents']; + $configuration = $form_state->getValue($component_form_parents); + $configuration['sources'][] = [ + 'source_id' => $source_id, + 'source' => [], + ]; + + $form_state->setValue($component_form_parents, $configuration); + $form_state->setRebuild(); + } + + /** + * Ajax handler: Refresh sources form. + */ + public static function refreshForm(array $form, FormStateInterface $form_state) { + $parents = $form_state->getTriggeringElement()['#ui_patterns_slot_array_parents']; + return NestedArray::getValue($form, $parents); + } + + /** + * Ajax submit handler: Remove source. + */ + public static function removeSource(array $form, FormStateInterface $form_state): void { + $trigger_element = $form_state->getTriggeringElement(); + $delta = $trigger_element['#delta']; + $component_form_parents = $trigger_element['#ui_patterns_slot_parents']; + $configuration = $form_state->getValue($component_form_parents); + unset($configuration['sources'][$delta]); + $form_state->setValue($component_form_parents, $configuration); + $form_state->setRebuild(); + } + +} diff --git a/src/Element/ComponentSlotsForm.php b/src/Element/ComponentSlotsForm.php index bd11c3879..880317746 100644 --- a/src/Element/ComponentSlotsForm.php +++ b/src/Element/ComponentSlotsForm.php @@ -2,38 +2,37 @@ namespace Drupal\ui_patterns\Element; -use Drupal\Component\Utility\Html; -use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Render\Element; -use Drupal\ui_patterns\SourcePluginBase; /** - * Provides a Component form builder element. - * - * Use #component_id to set an unchangeable component. - * - * The value of the form element contains following keys: - * [ - * 'slots' => [ - * 'slots_id' => [ - * ['source_id' => 'id', 'value' => 'Source value'] - * ], - * ], - * ] - * The same component value is returned in the form element. + * Component to render slots for a component. * * Usage example: * * @code * $form['slots'] = [ * '#type' => 'component_slots_form', + * '#component_id' => 'id' * '#default_value' => [ * 'slots' => [], * ], * ]; * @endcode * + * Value example: + * + * @code + * ['#default_value' => + * 'slots' => [ + * 'slots_id' => [ + * ['sources' => + * ['source_id' => 'id', 'value' => []] + * ] + * ], + * ], + * ] + * @endcode + * * @FormElement("component_slots_form") */ class ComponentSlotsForm extends ComponentFormBase { @@ -47,7 +46,9 @@ class ComponentSlotsForm extends ComponentFormBase { '#input' => TRUE, '#multiple' => FALSE, '#default_value' => NULL, + '#component_id' => NULL, '#source_contexts' => [], + '#tag_filter' => [], '#process' => [ [$class, 'buildForm'], ], @@ -60,20 +61,6 @@ class ComponentSlotsForm extends ComponentFormBase { */ public static function buildForm(array &$element, FormStateInterface $form_state): array { - /** @var \Drupal\ui_patterns\SourcePluginManager $sources_manager */ - $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source"); - $source_contexts = $element['#source_contexts'] ?? []; - $sources = $sources_manager->getDefinitionsForPropType('slot', $source_contexts); - $source_ids = array_keys($sources); - $source_ids = array_combine($source_ids, $source_ids); - $valid_source_plugins = $sources_manager->createInstances( - $source_ids, - SourcePluginBase::buildConfiguration('slot', [], [], $source_contexts, NULL), - ); - $options = []; - foreach ($valid_source_plugins as $valid_source_plugin) { - $options[$valid_source_plugin->getPluginId()] = $valid_source_plugin->label(); - } $component = static::getComponent($element); if (!isset($component->metadata->slots) || count( $component->metadata->slots @@ -81,284 +68,20 @@ class ComponentSlotsForm extends ComponentFormBase { hide($element); return $element; } + $contexts = $element['#source_contexts'] ?? []; $configuration = $element['#default_value']['slots'] ?? []; foreach ($component->metadata->slots as $slot_id => $slot) { - $element[$slot_id] = static::buildSlotForm($element, $form_state, $slot_id, $slot, $configuration[$slot_id] ?? [], $options, $source_contexts); - } - return $element; - } - - /** - * Build single slot form. - */ - protected static function buildSlotForm(array $element, FormStateInterface $form_state, string $slot_id, array $definition, array $configuration, array $options, array $source_contexts): array { - $form = []; - $wrapper_id = static::getElementId($element, 'ui-patterns-slot-' . $slot_id); - $form['#slot_id'] = $slot_id; - $form['sources'] = static::buildSourcesForm($element, $form_state, $slot_id, $definition, $configuration, $wrapper_id, $source_contexts); - $form['add_more_button'] = static::buildSourceSelector($element, $slot_id, $wrapper_id, $options); - return $form; - } - - /** - * Build single slot's sources form. - */ - protected static function buildSourcesForm(array $element, FormStateInterface $form_state, string $slot_id, array $definition, array $configuration, string $wrapper_id, array $source_contexts): array { - $form = [ - '#theme' => 'field_multiple_value_form', - '#title' => $definition['title'], - '#cardinality_multiple' => TRUE, - '#prefix' => '<div id="' . $wrapper_id . '">', - '#suffix' => '</div>', - ]; - // Add fake #field_name to avoid errors from - // template_preprocess_field_multiple_value_form. - $form['#field_name'] = "foo"; - if (!isset($configuration['sources'])) { - return $form; - } - foreach ($configuration['sources'] as $delta => $source_configuration) { - if (!isset($source_configuration['source_id'])) { - continue; - } - $form[$delta] = static::buildSourceForm($element, $form_state, $slot_id, $definition, $source_configuration, $delta, $wrapper_id, $source_contexts); - } - return $form; - } - - /** - * Build single source form. - */ - protected static function buildSourceForm(array $element, FormStateInterface $form_state, string $slot_id, array $definition, array $configuration, int $delta, string $wrapper_id, array $source_contexts): array { - $form_array_parents = $element["#array_parents"]; - $form_array_parents[] = $slot_id; - $form_array_parents[] = $delta; - $form = []; - $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source"); - $source = $sources_manager->createInstance( - $configuration['source_id'], - SourcePluginBase::buildConfiguration($slot_id, $definition, $configuration, $source_contexts, $form_array_parents) - ); - $form['source'] = $source->settingsForm([], $form_state); - $form['source_id'] = [ - '#type' => 'hidden', - '#value' => $source->getPluginId(), - ]; - $form['_weight'] = [ - '#type' => 'weight', - '#title' => t( - 'Weight for row @number', - ['@number' => $delta + 1] - ), - '#title_display' => 'invisible', - '#delta' => count($form), - '#default_value' => $configuration['_weight'] ?? $delta, - '#weight' => 100, - ]; - $form['_remove'] = static::buildRemoveSourceButton($element, $slot_id, $wrapper_id, $delta); - return $form; - } - - /** - * Build widget to remove source. - */ - protected static function buildRemoveSourceButton(array $element, string $slot_id, string $wrapper_id, int $delta): array { - $remove_action = [ - '#type' => 'submit', - '#name' => strtr($slot_id, '-', '_') . $delta . '_remove', - '#value' => t('Remove'), - '#submit' => [static::class . '::removeSource'], - '#access' => TRUE, - '#delta' => $delta, - '#ui_patterns' => [ - 'parents' => 5, - ], - '#slot_id' => $slot_id, - '#ajax' => [ - 'callback' => [static::class, 'refreshAfterSourceRemoval'], - 'wrapper' => $wrapper_id, - 'effect' => 'fade', - ], - ]; - return [ - '#type' => 'ui_patterns_actions', - '#ui_patterns_header' => TRUE, - 'dropdown_actions' => [ - static::expandComponentButton($element, $remove_action), - ], - ]; - } - - /** - * Build source selector. - */ - protected static function buildSourceSelector(array $element, string $slot_id, string $wrapper_id, array $options): array { - $action_buttons = []; - foreach ($options as $source_id => $source_label) { - $unique_name = implode("_", array_merge($element["#array_parents"], [$slot_id, $source_id, "add_more"])); - $action_buttons[$source_id] = static::expandComponentButton($element, [ - '#type' => 'submit', - // strtr($slot_id, '-', '_') . $source_id . '_add_more',. - '#name' => $unique_name, - '#value' => t('Add %source', ['%source' => $source_label]), - '#submit' => [ - static::class . '::addSource', - ], - '#access' => TRUE, - '#ui_patterns' => TRUE, + $element[$slot_id] = [ + '#title' => $slot['title'] ?? '', + '#type' => 'component_slot_form', + '#default_value' => $configuration[$slot_id] ?? [], + '#component_id' => $component->getPluginId(), '#slot_id' => $slot_id, - '#source_id' => $source_id, - '#ajax' => [ - 'callback' => [ - static::class, - 'refreshAfterSourceAddition', - ], - - 'wrapper' => $wrapper_id, - 'effect' => 'fade', - ], - ]); - } - return static::buildComponentDropbutton($action_buttons); - } - - /** - * Ajax submit handler: Add source. - */ - public static function addSource(array $form, FormStateInterface $form_state) { - $trigger_element = $form_state->getTriggeringElement(); - $slot_id = $trigger_element['#slot_id']; - $source_id = $trigger_element['#source_id']; - $component_form_parents = static::getComponentFormStateParents($trigger_element['#parents']); - $configuration = $form_state->getValue($component_form_parents); - $configuration['slots'][$slot_id]['sources'][] = [ - 'source_id' => $source_id, - 'source' => [], - ]; - $component_form_parents[] = "slots"; - $form_state->setValue($component_form_parents, $configuration["slots"]); - $form_state->setRebuild(); - } - - /** - * Ajax handler: Refresh sources form. - */ - public static function refreshAfterSourceAddition(array $form, FormStateInterface $form_state) { - $parents = $form_state->getTriggeringElement()['#array_parents']; - $form_layers = count([ - "(source)", - "add_more_button", - ]); - $form = NestedArray::getValue($form, array_slice($parents, 0, -$form_layers)); - if (!array_key_exists("sources", $form) || !is_array($form["sources"])) { - return []; - } - return $form['sources']; - } - - /** - * Ajax submit handler: Remove source. - */ - public static function removeSource(array $form, FormStateInterface $form_state): void { - $trigger_element = $form_state->getTriggeringElement(); - $delta = $trigger_element['#delta']; - $slot_id = $trigger_element['#slot_id']; - $component_form_parents = static::getComponentFormStateParents($trigger_element['#parents']); - $configuration = $form_state->getValue($component_form_parents); - unset($configuration['slots'][$slot_id]['sources'][$delta]); - $form_state->setValue($component_form_parents, $configuration); - $form_state->setRebuild(); - } - - /** - * Ajax handler: Refresh sources form. - */ - final public static function refreshAfterSourceRemoval(array $form, FormStateInterface $form_state) { - $parents = $form_state->getTriggeringElement()['#array_parents']; - $form_layers = count([ - "sources", - "(delta)", - "_remove", - "dropdown_actions", - ]); - return NestedArray::getValue($form, array_slice($parents, 0, -$form_layers)); - } - - /** - * Build drop button. - * - * @param array $elements - * Elements for drop button. - * - * @return array - * Drop button array. - */ - protected static function buildComponentDropbutton(array $elements = []): array { - $build = [ - '#type' => 'container', - '#attributes' => ['class' => ['ui-patterns-dropbutton-wrapper']], - ]; - - $operations = []; - // Because we are cloning the elements into title sub element we need to - // sort children first. - foreach (Element::children($elements, TRUE) as $child) { - // Clone the element as an operation. - $operations[$child] = ['title' => $elements[$child]]; - - // Flag the original element as printed so it doesn't render twice. - $elements[$child]['#printed'] = TRUE; - } - - $build['operations'] = [ - '#type' => 'ui_patterns_operations', - // Even though operations are run through the "links" element type, the - // theme system will render any render array passed as a link "title". - '#links' => $operations, - '#dropbutton_type' => 'small', - ]; - - return $build + $elements; - } - - /** - * Expand button base array into a paragraph widget action button. - * - * @param array $element - * Element. - * @param array $button_base - * Button base render array. - * - * @return array - * Button render array. - */ - protected static function expandComponentButton(array $element, array $button_base): array { - // Do not expand elements that do not have submit handler. - if (empty($button_base['#submit'])) { - return $button_base; - } - - $button = $button_base + [ - '#type' => 'submit', - '#theme_wrappers' => ['input__submit__ui_patterns_action'], - ]; - - // Html::getId will give us '-' char in name but we want '_' for now so - // we use strtr to search&replace '-' to '_'. - $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_'); - $button['#id'] = static::getElementId($element, $button['#name']); - - if (isset($button['#ajax'])) { - $button['#ajax'] += [ - 'effect' => 'fade', - // Since a normal throbber is added inline, this has the potential to - // break a layout if the button is located in dropbuttons. Instead, - // it's safer to just show the fullscreen progress element instead. - 'progress' => ['type' => 'fullscreen'], + '#source_contexts' => $contexts, + '#tag_filter' => $element['#tag_filter'], ]; } - - return static::expandAjax($button); + return $element; } } -- GitLab