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']);