Skip to content
Snippets Groups Projects
Commit 51cdafbe authored by christian.wiedemann's avatar christian.wiedemann
Browse files

Issue #3455074 by Christian.wiedemann: [2.0.0-alpha3] Refactoring of form elements

parent 0cc17941
Branches
Tags
1 merge request!117Resolve #3455074 "2.0.0 alpha3 refactoring of"
......@@ -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' => [
......
......@@ -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;
}
}
<?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);
}
}
......@@ -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"];
}
}
<?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();
}
}
......@@ -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;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment