diff --git a/config/schema/shs.schema.yml b/config/schema/shs.schema.yml index 3c7444a..d18dcc6 100644 --- a/config/schema/shs.schema.yml +++ b/config/schema/shs.schema.yml @@ -16,3 +16,6 @@ field.widget.settings.options_shs: force_deepest: type: boolean label: 'Force selection of deepest level' + save_lineage: + type: boolean + label: 'Save term lineage' \ No newline at end of file diff --git a/js/views/AddNewView.js b/js/views/AddNewView.js index 46370f4..a15c2f2 100644 --- a/js/views/AddNewView.js +++ b/js/views/AddNewView.js @@ -62,6 +62,10 @@ return; } + if (this.app.getSetting('hideAddAnotherButton')) { + return; + } + // Create "button". var $button = $('') .addClass('button') diff --git a/shs.module b/shs.module index 2d5d7f8..bdae9ce 100644 --- a/shs.module +++ b/shs.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Database\Database; +use Drupal\shs\Utility\TermLineage; /** * List all javascript classes used to create models and views in the widget. @@ -144,3 +145,35 @@ function shs_views_plugins_filter_alter(array &$plugins) { } } } + +/** + * Implements hook_shs_js_settings_alter + * @param array $settings_shs SHS element settings + * @param string $bundle Vocabulary ID + * @param string $field_name Field API name + * @return void + */ +function shs_shs_js_settings_alter(&$settings_shs, $bundle, $field_name) { + $widget_defaults = \Drupal::service('shs.widget_defaults'); + // Alter default value if save lineage is set + if (!empty($settings_shs['settings']['save_lineage'])) { + // If has a default value + if (isset($settings_shs['defaultValue'])) { + // Update default SHS value to the expected "leaf" child term set only + $params = array('vid' => $bundle); + // Full set is saved in db, reduce to leaf terms for widget + $handler = new TermLineage(); + $lineages = $handler->getLineages($settings_shs['defaultValue'], $params); + // Get set of deepest child terms only + $leaves = $handler->getLineageLeaves($lineages); + // Get array of tids + $tids = array_keys($leaves); + // Set default value + $settings_shs['defaultValue'] = array_keys($leaves); + // Refresh parent set + $parents = $widget_defaults->getParentDefaults($tids, $settings_shs['settings'], 'taxonomy_term'); + // Set parents + $settings_shs['parents'] = $parents; + } + } +} diff --git a/src/Plugin/Field/FieldWidget/OptionsShsWidget.php b/src/Plugin/Field/FieldWidget/OptionsShsWidget.php index 3bb51cf..be3987b 100644 --- a/src/Plugin/Field/FieldWidget/OptionsShsWidget.php +++ b/src/Plugin/Field/FieldWidget/OptionsShsWidget.php @@ -10,6 +10,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\shs\WidgetDefaults; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\shs\Utility\TermLineage; /** * Plugin implementation of the 'options_shs' widget. @@ -76,6 +77,8 @@ public static function defaultSettings() { 'create_new_items' => FALSE, 'create_new_levels' => FALSE, 'force_deepest' => FALSE, + 'save_lineage' => FALSE, + 'hide_add_another_item_button' => FALSE, ]; return $settings_default + parent::defaultSettings(); } @@ -111,6 +114,17 @@ public function settingsForm(array $form, FormStateInterface $form_state) { '#default_value' => $this->getSetting('force_deepest'), '#description' => t('Force users to select terms from the deepest level.'), ]; + $element['save_lineage'] = [ + '#type' => 'checkbox', + '#title' => t('Save lineage'), + '#default_value' => $this->getSetting('save_lineage'), + '#description' => t('Save term lineage.'), + ]; + $element['hide_add_another_item_button'] = [ + '#type' => 'checkbox', + '#title' => t('Hide add another button'), + '#default_value' => $this->getSetting('hide_add_another_item_button'), + ]; return $element; } @@ -139,6 +153,15 @@ public function settingsSummary() { else { $summary[] = t('Do not force selection of deepest level'); } + if ($this->getSetting('save_lineage')) { + $summary[] = t('Save lineage'); + } + else { + $summary[] = t('Do not save lineage'); + } + if ($this->getSetting('hide_add_another_item_button')) { + $summary[] = t('Hide add another button'); + } return $summary; } @@ -159,6 +182,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen if (isset($user_input[$field_name])) { $default_value = $user_input[$field_name]; } + $target_bundles = $this->getFieldSetting('handler_settings')['target_bundles']; $settings_additional = [ 'required' => $this->required, @@ -166,6 +190,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen 'anyLabel' => $this->getEmptyLabel() ?: t('- None -'), 'anyValue' => '_none', 'addNewLabel' => t('Add another item'), + 'hideAddAnotherButton' => $this->getSetting('hide_add_another_item_button'), ]; $bundle = reset($target_bundles); @@ -272,6 +297,21 @@ public static function validateElement(array $element, FormStateInterface $form_ if (!is_array($value)) { $value = [$value]; } + + // If the terms lineage is stored the parents should be removed. + if (!empty($value) && !empty($element['#shs']['settings']['save_lineage'])) { + /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */ + $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); + foreach ($value as $key => $item) { + if (($children = $term_storage->loadChildren($item))) { + if (array_intersect(array_keys($children), $value)) { + unset($value[$key]); + } + } + } + $value = array_values($value); + } + if ($element['#shs']['settings']['anyValue'] === reset($value)) { if (!$element['#required']) { return; @@ -319,4 +359,46 @@ protected function settingToString($key) { return $options[$value]; } + /** + * Massage form values into field item schema data structure for storage + * @param array $values widget values + * @param array $form form definition + * @param FormStateInterface $form_state form state + * @return array value set for save + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + // Check save lineage setting + $save_lineage = $this->getSetting('save_lineage'); + if ($save_lineage) { + // Term id list + $tids = array(); + // Vocabulary ID + $vid = FALSE; + // Get tid set + if (!empty($values)) { + foreach ($values as $delta => $value) { + if (isset($value['target_id'])) { + // Store target tid + $tids[] = $value['target_id']; + if (empty($vocab)) { + // Load vid once, simpler to get the vocab ref this way + $term = taxonomy_term_load($value['target_id']); + $vid = $term->getVocabularyId(); + } + } + } + } + // Get term lineage from selected tid set + $handler = new TermLineage(); + $params = array('vid' => $vid); + $lineages = $handler->getLineages($tids, $params); + // Get tid list from lineage sets + $tid_list = $handler->getFlatLineage($lineages); + // Save lineage set to values + $values = array_keys($tid_list); + } + // Return massaged field widget values + return $values; + } + } diff --git a/src/Utility/TermLineage.php b/src/Utility/TermLineage.php new file mode 100644 index 0000000..69740c2 --- /dev/null +++ b/src/Utility/TermLineage.php @@ -0,0 +1,224 @@ +getStorage('taxonomy_term') + ->loadTree($params['vid'], 0, 1); + + // If the root_term parameter is enabled, then prepend a fake "" term. + if (isset($params['root_term']) && $params['root_term'] === TRUE) { + $root_term = new StdClass(); + $root_term->tid = 0; + $root_term->name = '<' . t('root') . '>'; + $terms = array_merge(array($root_term), $terms); + } + // Format array + return $this->formatTermOptions($terms); + } + + /** + * Helper function to construct the lineages given a set of selected items + * + * @credit Based on Hierarchical Select from Drupal 7 + * + * @param array $selection + * Array of taxonomy term ids + * @param array $params + * Optional. An array of parameters, including vid = machine name + * @return + * An array of taxonomy term lineages. + */ + public function getLineages($selection, $params) { + // We have to reconstruct all lineages from the given set of selected items. + // That means: we have to reconstruct every possible combination! + $lineages = array(); + $root_level = $this->getRootLevel($params); + $level = -1; + $stored_parents = array(); + + foreach ($selection as $key => $item) { + // Create new lineage if the item can be found in the root level. + if (array_key_exists($item, $root_level)) { + $level++; + $lineages[$level][0] = array('value' => $item, 'label' => $root_level[$item]); + $children = $this->getChildren($item, $params); + // Try to find all the selected term which is under current root term. + foreach ($selection as $key2 => $item2) { + if (!isset($stored_parents[$item2]) && isset($children[$item2])) { + $stored_parents[$item2] = array('parent' => $lineages[$level][0], 'label' => $children[$item2]); + } + } + } + // Add the term in current level when it's children of the parent term. + elseif (isset($children[$item])) { + $lineage = array('value' => $item, 'label' => $children[$item]); + $lineages[$level][] = $lineage; + // Try to find all the selected term which is under current term. + $children = $this->getChildren($item, $params); + foreach ($selection as $key2 => $item2) { + if (!isset($stored_parents[$item2]) && isset($children[$item2])) { + $stored_parents[$item2] = array('parent' => $lineage, 'label' => $children[$item2]); + } + } + } + // If the current term can't be found in the root level and not children of the previous term. + // That means: Current term sharing the same parent of the previous term and we stored the information already. + elseif (isset($stored_parents[$item])) { + $level++; + $lineage = array('value' => $item, 'label' => $stored_parents[$item]['label']); + $lineages[$level][] = $lineage; + // Try to find all the selected term which is under current term. + $children = $this->getChildren($item, $params); + foreach ($selection as $key2 => $item2) { + if (!isset($stored_parents[$item2]) && isset($children[$item2])) { + $stored_parents[$item2] = array('parent' => $lineage, 'label' => $children[$item2]); + } + } + // Find all the parent terms. + while (isset($stored_parents[$item])) { + if (isset($stored_parents[$item]['parent'])) { + $lineages[$level][] = array('value' => $stored_parents[$item]['parent']['value'], 'label' => $stored_parents[$item]['parent']['label']); + $item = $stored_parents[$item]['parent']['value']; + } + else { + break; + } + } + $lineages[$level] = array_reverse($lineages[$level]); + } + // No parent term found for current item, let's find them. + elseif ($term = taxonomy_term_load($item)) { + $level++; + $lineage = array('value' => $item, 'label' => $term->getName()); + $lineages[$level][] = $lineage; + // Try to find all the selected term which is under current term. + $children = $this->getChildren($item, $params); + foreach ($selection as $key2 => $item2) { + if (!isset($stored_parents[$item2]) && isset($children[$item2])) { + $stored_parents[$item2] = array('parent' => $lineage, 'label' => $children[$item2]); + } + } + if ($parents = $this->getParents($item)) { + foreach($parents as $parent_tid => $parent_name) { + if ($parent_tid == $item) { + continue; + } + $lineages[$level][] = array('value' => $parent_tid, 'label' => $parent_name); + } + } + $lineages[$level] = array_reverse($lineages[$level]); + } + } + return $lineages; + } + + /** + * Get parent terms by tid + * @param int $tid term ID + * @return array list of term parent tid => name + */ + public function getParents($tid) { + $ancestors = \Drupal::service('entity_type.manager') + ->getStorage('taxonomy_term') + ->loadAllParents($tid); + $parents = []; + foreach ($ancestors as $ancestor) { + $parents[$ancestor->id()] = $ancestor->getName(); + } + return $parents; + } + + /** + * Implementation of hook_hierarchical_select_children(). + */ + public function getChildren($parent, $params) { + if (isset($params['root_term']) && $params['root_term'] && $parent == 0) { + return array(); + } + + // Get child terms under parent + $terms = \Drupal::service('entity_type.manager') + ->getStorage('taxonomy_term') + ->loadTree($params['vid'], $parent); + + return $this->formatTermOptions($terms); + } + + /** + * Get the child term leaves from each lineage + * @param array $lineages term lineage data structure + * @return array lineage leaf term tid => name + */ + public function getLineageLeaves($lineages) { + $leaves = array(); + if (empty($lineages)) { + return $leaves; + } + foreach ($lineages as $key => $lineage) { + $leaf = array_pop($lineage); + $leaves[$leaf['value']] = $leaf['label']; + } + return $leaves; + } + + /** + * Get the child term leaves from each lineage + * @param array $lineages term lineage data structure + * @return array flat list of lineage tids + */ + public function getFlatLineage($lineages) { + $tids = array(); + if (empty($lineages)) { + return $tids; + } + foreach ($lineages as $key => $lineage) { + foreach ($lineage as $key2 => $leaf) { + $tids[$leaf['value']] = $leaf['label']; + } + } + return $tids; + } + + /** + * Transform an array of terms into an associative array of options, for use + * in a select form item. + * + * @param $terms + * An array of term objects. + * @return + * An associative array of options, keys are tids, values are term names. + */ + public function formatTermOptions($terms) { + $options = array(); + foreach ($terms as $key => $term) { + if (isset($term->tid) && isset($term->name)) { + $options[$term->tid] = $term->name; + } else { + $options[$term->id()] = $term->getName(); + } + } + return $options; + } + + +}