View source
<?php
namespace Drupal\cshs;
use Drupal\Core\Entity\FieldableEntityStorageInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Html;
use Drupal\cshs\Element\CshsElement;
use Drupal\taxonomy\VocabularyInterface;
use Drupal\field\FieldStorageConfigInterface;
const HIERARCHY_OPTIONS = [
'hierarchy_depth' => [
'Hierarchy depth',
[
'Limits the nesting level. Use 0 to display all values. For the hierarchy like',
'"a" -> "b" -> "c" the selection of 2 will result in "b" being the deepest option.',
],
],
'required_depth' => [
'Required depth',
[
'Requires item selection at the given nesting level. Use 0 to not impose the',
'requirement. For the hierarchy like "a" -> "b" -> "c" the selection of 2 will',
'obey a user to select at least "a" and "b".',
],
],
];
trait CshsOptionsFromHelper {
use TaxonomyStorages;
public static function defaultSettings() : array {
return [
'parent' => 0,
'level_labels' => '',
'force_deepest' => FALSE,
'save_lineage' => FALSE,
'hierarchy_depth' => 0,
'required_depth' => 0,
'none_label' => CshsElement::NONE_LABEL,
];
}
public abstract function getSettings() : array;
public abstract function getSetting($key);
public abstract function getVocabulary() : ?VocabularyInterface;
public function settingsSummary() : array {
$settings = $this
->getSettings();
$summary = [];
$deepest = $this
->t('Deepest');
$none = $this
->t('None');
$yes = $this
->t('Yes');
$no = $this
->t('No');
$summary[] = $this
->t('Parent: @parent', [
'@parent' => empty($settings['parent']) ? $none : $this
->getTranslationFromContext($this
->getTermStorage()
->load($settings['parent']))
->label(),
]);
foreach (HIERARCHY_OPTIONS as $option_name => [
$title,
]) {
$summary[] = $this
->t("{$title}: @{$option_name}", [
"@{$option_name}" => empty($settings['force_deepest']) ? empty($settings[$option_name]) ? $none : $settings[$option_name] : $deepest,
]);
}
$summary[] = $this
->t('Force deepest: @force_deepest', [
'@force_deepest' => empty($settings['force_deepest']) ? $no : $yes,
]);
$summary[] = $this
->t('Save lineage: @save_lineage', [
'@save_lineage' => empty($settings['save_lineage']) ? $no : $yes,
]);
$summary[] = $this
->t('Level labels: @level_labels', [
'@level_labels' => empty($settings['level_labels']) ? $none : $this
->getTranslatedLevelLabels(),
]);
$summary[] = $this
->t('The "no selection" label: @none_label', [
'@none_label' => $this
->getTranslatedNoneLabel(),
]);
return $summary;
}
public function settingsForm(array $form, FormStateInterface $form_state) : array {
$vocabulary = $this
->getVocabulary();
\assert($vocabulary !== NULL);
$options = [];
foreach ($this
->getOptions($vocabulary
->id()) as $key => $value) {
$options[$key] = $value['name'];
}
$element['parent'] = [
'#type' => 'select',
'#title' => $this
->t('Parent'),
'#options' => $options,
'#description' => $this
->t('Select a parent term to use only a subtree of a vocabulary for this field.'),
'#default_value' => $this
->getSetting('parent'),
];
foreach (HIERARCHY_OPTIONS as $option_name => [
$title,
$description,
]) {
$description[] = '<i>Ignored when the deepest selection is enforced.</i>';
$element[$option_name] = [
'#min' => 0,
'#type' => 'number',
'#title' => $title,
'#description' => $this
->t(\implode('<br />', $description)),
'#default_value' => $this
->getSetting($option_name),
'#states' => [
'disabled' => [
':input[name*="force_deepest"]' => [
'checked' => TRUE,
],
],
],
];
}
$element['force_deepest'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Force selection of deepest level'),
'#description' => $this
->t('If checked the user will be forced to select terms from the deepest level.'),
'#default_value' => $this
->getSetting('force_deepest'),
];
if ($this instanceof WidgetBase) {
$errors = [];
$field_storage = $this->fieldDefinition
->getFieldStorageDefinition();
\assert($field_storage instanceof FieldStorageConfigInterface);
$entity_storage = $this
->getStorage($field_storage
->getTargetEntityTypeId());
\assert($entity_storage instanceof FieldableEntityStorageInterface);
$is_unlimited = $field_storage
->getCardinality() === FieldStorageConfigInterface::CARDINALITY_UNLIMITED;
$description = [
'Save all parents of the selected term. Please note that you will not have a',
'familiar field when multiple items can be added via the "Add more" button.',
'In fact, the field will look like a "single" and the selected terms will',
'be stored each as a separate field value.',
'',
'<b>Keep in mind</b> that this setting cannot be changed once the field storage',
'gets at least one item. The restriction is imposed across all bundles of',
'the "' . $entity_storage
->getEntityType()
->getLabel() . '" entity type.',
];
if ($entity_storage
->countFieldData($field_storage, TRUE)) {
$errors[] = 'There is data for this field in the database. This setting can no longer be changed.';
}
if (!$is_unlimited) {
$errors[] = 'The option can be enabled only for fields with unlimited cardinality.';
}
if (!empty($errors)) {
\array_unshift($description, \sprintf('<b class="color-error">%s</b>', \implode('<br />', $errors)));
}
$element['save_lineage'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Save lineage'),
'#disabled' => !empty($errors),
'#description' => $this
->t(\implode('<br />', $description)),
'#default_value' => $is_unlimited && $this
->getSetting('save_lineage'),
];
}
$element['level_labels'] = [
'#type' => 'textfield',
'#title' => $this
->t('Labels per hierarchy-level'),
'#description' => $this
->t('Enter labels for each hierarchy-level separated by comma.'),
'#default_value' => $this
->getTranslatedLevelLabels(),
];
$element['none_label'] = [
'#type' => 'textfield',
'#title' => $this
->t('The "no selection" label'),
'#description' => $this
->t('The label for an empty option.'),
'#default_value' => $this
->getTranslatedNoneLabel(),
];
$element['#element_validate'][] = [
$this,
'validateSettingsForm',
];
$form_state
->set('vocabulary', $vocabulary);
return $element;
}
public function validateSettingsForm(array &$element, FormStateInterface $form_state) : void {
$settings = $form_state
->getValue($element['#parents']);
$options = $element['parent']['#options'];
foreach ($options as $id => $label) {
unset($options[$id]);
if ((string) $id === $settings['parent']) {
break;
}
}
if ($settings['hierarchy_depth'] > ($max_hierarchy_depth = \count($options))) {
$form_state
->setError($element['hierarchy_depth'], $this
->t('The hierarchy depth cannot be @actual because the selection list has @levels levels.', [
'@actual' => $settings['hierarchy_depth'],
'@levels' => $max_hierarchy_depth,
]));
}
elseif ($settings['required_depth'] > $settings['hierarchy_depth']) {
$form_state
->setError($element['required_depth'], $this
->t('The required depth cannot be greater than the hierarchy depth.'));
}
}
public function formElement() : array {
$vocabulary = $this
->getVocabulary();
\assert($vocabulary !== NULL);
$settings = $this
->getSettings();
if (!empty($settings['force_deepest']) || ($max_depth = $settings['hierarchy_depth']) < 1) {
$max_depth = NULL;
}
return [
'#type' => CshsElement::ID,
'#labels' => $this
->getTranslatedLevelLabels(FALSE),
'#parent' => $settings['parent'],
'#options' => $this
->getOptions($vocabulary
->id(), $settings['parent'], CshsElement::NONE_VALUE, $max_depth),
'#multiple' => $settings['save_lineage'],
'#vocabulary' => $vocabulary,
'#none_value' => CshsElement::NONE_VALUE,
'#none_label' => $this
->getTranslatedNoneLabel(),
'#default_value' => CshsElement::NONE_VALUE,
'#force_deepest' => $settings['force_deepest'],
'#required_depth' => $settings['required_depth'],
];
}
private function getOptions(string $vocabulary_id, int $parent = 0, $none_value = 0, int $max_depth = NULL) : array {
\assert(\is_int($none_value) || \is_string($none_value));
$cache =& \drupal_static(__METHOD__, []);
$cache_id = "{$vocabulary_id}:{$parent}:{$none_value}:{$max_depth}";
if (!isset($cache[$cache_id])) {
$storage = $this
->getTermStorage();
$cache[$cache_id] = [
$none_value => CshsElement::option($this
->t(CshsElement::NONE_LABEL)),
];
if ($this
->needsTranslatedContent()) {
$get_name = function (object $term) use ($storage) : string {
return $this
->getTranslationFromContext($storage
->load($term->tid))
->label();
};
}
else {
$get_name = static function (object $term) : string {
return $term->name;
};
}
foreach ($storage
->loadTree($vocabulary_id, $parent, $max_depth, FALSE) as $term) {
\assert($term instanceof \stdClass);
\assert(\is_array($term->parents));
\assert(\is_numeric($term->status));
\assert(\is_numeric($term->depth));
\assert(\is_numeric($term->tid));
\assert(\is_string($term->name));
if ((bool) $term->status) {
$parents = \array_values($term->parents);
$cache[$cache_id][$term->tid] = CshsElement::option($get_name($term), (int) \reset($parents));
}
}
}
return $cache[$cache_id];
}
private function getTranslatedLevelLabels(bool $return_as_string = TRUE) {
$labels = $this
->getSetting('level_labels');
if (empty($labels)) {
return $return_as_string ? '' : [];
}
$labels = \array_map([
$this,
'getTranslatedValue',
], Tags::explode($labels));
return $return_as_string ? \implode(', ', $labels) : $labels;
}
private function getTranslatedNoneLabel() : string {
return $this
->getTranslatedValue($this
->getSetting('none_label') ?: CshsElement::NONE_LABEL);
}
private function getTranslatedValue(string $value) : string {
$value = \trim($value);
return $value === '' ? $value : (string) $this
->t(Html::escape($value));
}
}