diff --git a/composer.json b/composer.json index b193527..a706100 100755 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "drupal/core": "^9 || ^10 || ^11", + "drupal/core": "^10.2 || ^11", "drupal/ds": "~3.3 || ^5.0@alpha", "drupal/book": "^1.0" }, @@ -22,5 +22,8 @@ "psr-4": { "Drupal\\Tests\\stanford_fields\\": "./tests/src" } + }, + "require-dev": { + "drupal/cshs": "^4.0" } } diff --git a/src/Plugin/Block/BookForwardBackBlock.php b/src/Plugin/Block/BookForwardBackBlock.php index b2b48a2..a5be3fc 100644 --- a/src/Plugin/Block/BookForwardBackBlock.php +++ b/src/Plugin/Block/BookForwardBackBlock.php @@ -2,23 +2,25 @@ namespace Drupal\stanford_fields\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Plugin\Context\EntityContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a 'Book navigation' block. - * - * @Block( - * id = "book_forward_back", - * admin_label = @Translation("Book Forward & Back"), - * category = @Translation("Book"), - * context_definitions = { - * "node" = @ContextDefinition("entity:node", label = @Translation("Node"), required = FALSE) - * } - * ) */ +#[Block( + id: "book_forward_back", + admin_label: new TranslatableMarkup("Book Forward & Back"), + category: new TranslatableMarkup("Book"), + context_definitions: [ + 'node' => new EntityContextDefinition(data_type: 'entity:node', label: new TranslatableMarkup("Node"), required: FALSE), + ] +)] class BookForwardBackBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/src/Plugin/Field/FieldFormatter/EntityTitleHeading.php b/src/Plugin/Field/FieldFormatter/EntityTitleHeading.php index a6121b8..5431734 100755 --- a/src/Plugin/Field/FieldFormatter/EntityTitleHeading.php +++ b/src/Plugin/Field/FieldFormatter/EntityTitleHeading.php @@ -2,6 +2,7 @@ namespace Drupal\stanford_fields\Plugin\Field\FieldFormatter; +use Drupal\Core\Field\Attribute\FieldFormatter; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FormatterBase; @@ -9,19 +10,17 @@ use Drupal\Core\Link; use Drupal\Core\Path\PathMatcherInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provide a string field to be used as a heading. - * - * @FieldFormatter( - * id = "entity_title_heading", - * label = @Translation("Heading"), - * field_types = { - * "string" - * } - * ) */ +#[FieldFormatter( + id: 'entity_title_heading', + label: new TranslatableMarkup('Heading'), + field_types: ['string'], +)] class EntityTitleHeading extends FormatterBase implements ContainerFactoryPluginInterface { /** diff --git a/src/Plugin/Field/FieldWidget/DateYearOnlyWidget.php b/src/Plugin/Field/FieldWidget/DateYearOnlyWidget.php index 8e798ab..90a586f 100644 --- a/src/Plugin/Field/FieldWidget/DateYearOnlyWidget.php +++ b/src/Plugin/Field/FieldWidget/DateYearOnlyWidget.php @@ -2,21 +2,20 @@ namespace Drupal\stanford_fields\Plugin\Field\FieldWidget; +use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\datetime\Plugin\Field\FieldWidget\DateTimeDatelistWidget; /** * Plugin to provide a date widget that only collects the year. - * - * @FieldWidget( - * id = "datetime_year_only", - * label = @Translation("Year Only"), - * field_types = { - * "datetime" - * } - * ) */ +#[FieldWidget( + id: 'datetime_year_only', + label: new TranslatableMarkup('Year Only'), + field_types: ['datetime'], +)] class DateYearOnlyWidget extends DateTimeDatelistWidget { /** diff --git a/src/Plugin/Field/FieldWidget/LocalistUrlWidget.php b/src/Plugin/Field/FieldWidget/LocalistUrlWidget.php index deefef5..8178161 100644 --- a/src/Plugin/Field/FieldWidget/LocalistUrlWidget.php +++ b/src/Plugin/Field/FieldWidget/LocalistUrlWidget.php @@ -5,10 +5,12 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Field\Attribute\FieldWidget; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\Url as UrlElement; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\link\Plugin\Field\FieldWidget\LinkWidget; use GuzzleHttp\ClientInterface; @@ -17,15 +19,12 @@ /** * Plugin implementation of the 'localist_url' widget. - * - * @FieldWidget( - * id = "localist_url", - * label = @Translation("Localist"), - * field_types = { - * "link" - * } - * ) */ +#[FieldWidget( + id: 'localist_url', + label: new TranslatableMarkup('Localist URL'), + field_types: ['link'], +)] class LocalistUrlWidget extends LinkWidget { /** diff --git a/src/Plugin/Field/FieldWidget/TaxonomyLabelHierarchyWidget.php b/src/Plugin/Field/FieldWidget/TaxonomyLabelHierarchyWidget.php new file mode 100644 index 0000000..4d7f4e0 --- /dev/null +++ b/src/Plugin/Field/FieldWidget/TaxonomyLabelHierarchyWidget.php @@ -0,0 +1,152 @@ +moduleExists('cshs'); + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array { + $element = parent::formElement($items, $delta, $element, $form, $form_state); + $options = $this->getOptions($items->getEntity()); + $selected_items = $this->getSelectedOptions($items); + + $element += [ + '#type' => 'fieldset', + '#tree' => TRUE, + ]; + + $grouped_options = []; + foreach ($options as $id => $option) { + if (!str_starts_with((string) $option, '-')) { + $parent = (string) $option; + continue; + } + $grouped_options[$parent][$id] = substr((string) $option, 1); + } + + foreach ($grouped_options as $group_label => $group_options) { + $key = preg_replace('@[^a-z0-9_.]+@', '_', mb_strtolower($group_label)); + foreach ($group_options as $tid => &$option) { + /** @var \Drupal\taxonomy\TermInterface $term */ + $term = \Drupal::entityTypeManager() + ->getStorage('taxonomy_term') + ->load($tid); + $option = new CshsOption(ltrim($option, "-"), $term->get('parent') + ->getString(), $group_label); + } + + $num_values = $form_state->get($key); + $default_values = array_values(array_intersect($selected_items, array_keys($group_options))); + + if (!$num_values) { + $num_values = max(1, count($default_values)); + $form_state->set($key, $num_values); + } + + $element[$key] = [ + '#type' => 'fieldset', + '#title' => $group_label, + '#prefix' => '
', + '#suffix' => '
', + ]; + + for ($i = 0; $i < $num_values; $i++) { + $element[$key][$i]['target_id'] = [ + '#type' => CshsElement::ID, + '#labels' => [], + '#options' => $group_options, + '#multiple' => $this->multiple, + '#default_value' => $default_values[$i] ?? NULL, + ]; + } + $element[$key]['add'] = [ + '#type' => 'submit', + '#value' => $this->t('Add More'), + '#name' => $key, + '#submit' => [[self::class, 'addOne']], + '#limit_validation_errors' => [], + '#ajax' => [ + 'callback' => [self::class, 'addMoreCallback'], + 'wrapper' => $key, + ], + ]; + } + + return $element; + } + + /** + * Ajax callback to add another. + * + * @codeCoverageIgnore + */ + public static function addMoreCallback(array &$form, FormStateInterface $form_state) { + $trigger = $form_state->getTriggeringElement(); + $path = $trigger['#array_parents']; + array_pop($path); + return NestedArray::getValue($form, $path); + } + + /** + * Form callback to add another. + * + * @codeCoverageIgnore + */ + public static function addOne(array &$form, FormStateInterface $form_state) { + $trigger = $form_state->getTriggeringElement(); + $name = $trigger['#name']; + $form_state->set($name, $form_state->get($name) + 1); + $form_state->setRebuild(); + } + + /** + * {@inheritdoc} + * + * @codeCoverageIgnore + */ + public static function validateElement(array $element, FormStateInterface $form_state) { + $value = []; + foreach (Element::children($element) as $key) { + foreach (Element::children($element[$key]) as $delta) { + if (isset($element[$key][$delta]['target_id']['#value']) && is_array($element[$key][$delta]['target_id']['#value'])) { + $value[] = end($element[$key][$delta]['target_id']['#value']); + } + } + } + $element['#value'] = array_filter(array_unique($value)); + parent::validateElement($element, $form_state); + } + +} diff --git a/src/Plugin/views/display/ViewFieldBlock.php b/src/Plugin/views/display/ViewFieldBlock.php index 1bbd8dc..86b9624 100644 --- a/src/Plugin/views/display/ViewFieldBlock.php +++ b/src/Plugin/views/display/ViewFieldBlock.php @@ -2,6 +2,8 @@ namespace Drupal\stanford_fields\Plugin\views\display; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\views\Attribute\ViewsDisplay; use Drupal\views\Plugin\views\display\Block; /** @@ -9,20 +11,19 @@ * * @ingroup views_display_plugins * - * @ViewsDisplay( - * id = "viewfield_block", - * title = @Translation("View Field Block"), - * help = @Translation("Identical to a block, but allows for granular viewfield settings."), - * theme = "views_view", - * register_theme = FALSE, - * uses_hook_block = TRUE, - * contextual_links_locations = {"block"}, - * admin = @Translation("Block") - * ) - * * @see \Drupal\views\Plugin\Block\ViewsBlock * @see \Drupal\views\Plugin\Derivative\ViewsBlock */ +#[ViewsDisplay( + id: "viewfield_block", + title: new TranslatableMarkup("View Field Block"), + admin: new TranslatableMarkup("Block"), + help: new TranslatableMarkup("Identical to a block, but allows for granular viewfield settings."), + theme: "views_view", + contextual_links_locations: ["block"], + register_theme: FALSE, + uses_hook_block: TRUE, +)] class ViewFieldBlock extends Block { /** diff --git a/stanford_fields.info.yml b/stanford_fields.info.yml index 8f13a6e..5ccb988 100755 --- a/stanford_fields.info.yml +++ b/stanford_fields.info.yml @@ -1,7 +1,7 @@ name: 'Stanford Fields' type: module description: 'Field types, widgets and formatters to enhance Drupal and Contrib.' -core_version_requirement: ^9 || ^10 || ^11 +core_version_requirement: ^10.2 || ^11 package: Stanford version: 8.2.7 dependencies: diff --git a/tests/src/Kernel/Plugin/Field/FieldWidget/TaxonomyLabelHierarchyWidgetTest.php b/tests/src/Kernel/Plugin/Field/FieldWidget/TaxonomyLabelHierarchyWidgetTest.php new file mode 100644 index 0000000..3465ca3 --- /dev/null +++ b/tests/src/Kernel/Plugin/Field/FieldWidget/TaxonomyLabelHierarchyWidgetTest.php @@ -0,0 +1,122 @@ + 'tag_terms', 'label' => 'terms'])->save(); + + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_terms', + 'entity_type' => 'node', + 'type' => 'entity_reference', + 'cardinality' => -1, + 'settings' => ['target_type' => 'taxonomy_term'], + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_name' => 'field_terms', + 'entity_type' => 'node', + 'bundle' => 'page', + 'settings' => [ + 'handler' => 'default:taxonomy_term', + 'handler_settings' => [ + 'target_bundles' => ['tag_terms' => 'tag_terms'], + ], + ], + ]); + $field->save(); + + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $entity_form_display */ + $entity_form_display = EntityFormDisplay::create([ + 'targetEntityType' => 'node', + 'bundle' => 'page', + 'mode' => 'default', + 'status' => TRUE, + ]); + $entity_form_display->setComponent('field_terms', [ + 'type' => 'taxonomy_label_hierarchy', + ])->removeComponent('created')->save(); + } + + /** + * Test the entity form is displayed correctly. + */ + public function testWidgetForm() { + $parent_term = Term::create([ + 'vid' => 'tag_terms', + 'name' => 'Parent', + 'parent' => [0], + ]); + $parent_term->save(); + + $second_parent_term = Term::create([ + 'vid' => 'tag_terms', + 'name' => 'Second Parent', + 'parent' => [0], + ]); + $second_parent_term->save(); + + $foo = Term::create([ + 'vid' => 'tag_terms', + 'name' => 'Foo', + 'parent' => [$parent_term->id()], + ]); + $foo->save(); + + $bar = Term::create([ + 'vid' => 'tag_terms', + 'name' => 'Bar', + 'parent' => [$parent_term->id()], + ]); + $bar->save(); + + $baz = Term::create([ + 'vid' => 'tag_terms', + 'name' => 'Baz', + 'parent' => [$second_parent_term->id()], + ]); + $baz->save(); + + $author = User::create(['name' => 'foo']); + $author->save(); + $node = Node::create(['type' => 'page', 'title' => 'foobar', 'uid' => $author->id()]); + $node->save(); + + /** @var \Drupal\Core\Entity\EntityFormBuilderInterface $form_builder */ + $form_builder = $this->container->get('entity.form_builder'); + $form = $form_builder->getForm($node); + + $widget_value = $form['field_terms']['widget']; + + $this->assertArrayHasKey($foo->id(), $widget_value['parent'][0]['target_id']['#options']); + $this->assertArrayHasKey($bar->id(), $widget_value['parent'][0]['target_id']['#options']); + $this->assertArrayNotHasKey($baz->id(), $widget_value['parent'][0]['target_id']['#options']); + + $this->assertArrayNotHasKey($foo->id(), $widget_value['second_parent'][0]['target_id']['#options']); + $this->assertArrayNotHasKey($bar->id(), $widget_value['second_parent'][0]['target_id']['#options']); + $this->assertArrayHasKey($baz->id(), $widget_value['second_parent'][0]['target_id']['#options']); + } + +} diff --git a/tests/src/Kernel/StanfordFieldKernelTestBase.php b/tests/src/Kernel/StanfordFieldKernelTestBase.php index 82f3210..a8d6aa4 100644 --- a/tests/src/Kernel/StanfordFieldKernelTestBase.php +++ b/tests/src/Kernel/StanfordFieldKernelTestBase.php @@ -22,6 +22,9 @@ class StanfordFieldKernelTestBase extends KernelTestBase { 'stanford_fields', 'field', 'link', + 'taxonomy', + 'text', + 'cshs', ]; /** @@ -33,6 +36,8 @@ public function setup(): void { $this->installEntitySchema('node'); $this->installEntitySchema('field_config'); $this->installEntitySchema('date_format'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('path_alias'); $this->installConfig(['system', 'field', 'link']); $this->installSchema('node', ['node_access']);