Disable Taxonomy Term Sorting

May 16, 2023

I have a vocabulary for which the terms should always be sorted by their date field, so I wanted to disable manual sorting. My first step was to apply the Flat Taxonomy functionality to the vocabulary to save having to worry about hierarchy when removing sorting. With that in place, I removed the Weight field from the term add/edit form (throughout this post the vocabulary I'm working on is called session):

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Remove the weight field, because sessions are always sorted by date.
 */
function MODULE_form_taxonomy_term_session_form_alter(array &$form, FormStateInterface $form_state) {
  if (isset($form['relations'])) {
    if (isset($form['relations']['weight'])) {
      $form['relations']['weight']['#access'] = FALSE;
    }
    // If there's nothing visible in the relations fieldset, remove it.
    $relation_fields_exist = FALSE;
    foreach ($form['relations'] as $key => $data) {
      if ((substr($key, 0, 1) != '#') && (!isset($data['#access']) || $data['#access'] !== FALSE)) {
        $relation_fields_exist = TRUE;
        break;
      }
    }
    if (!$relation_fields_exist) {
      $form['relations']['#access'] = FALSE;
    }
  }
}

Most of this isn't actually removing the Weight field, that could be done in three lines, but Flat Taxonomy removes the Parent functionality from the term form Relations section, so unless another module has added something there removing the weight field probably leaves it empty, in which case it might as well be removed.

Each individual term can now no longer have its weight set but the term list page still enables term ordering, so that has to be removed as well:

/**
 * Implements hook_form_FORM_ID_alter();
 *
 * Remove sorting functionality from the session list and sort by date.
 */
function MODULE_form_taxonomy_overview_terms_alter(&$form, &$form_state, $form_id) {
  if (($taxonomy_vocabulary_id = _MODULE_vocabulary_id_from_overview_route()) && ($taxonomy_vocabulary_id == 'session') && (isset($form['terms'])) && ($form['terms']) && ($sorted_term_ids = _MODULE_sorted_term_ids())) {
    // Field keys in $form['terms'] look like "tid:[tid]:0", but I'm not sure
    // where the "0" comes from and if it's reliable, so build a reference array
    // excluding that component.
    $reference_term_ids = [];
    foreach ($form['terms'] as $key => $data) {
      $key_parts = explode(':', $key);
      if ((count($key_parts) == 3) && (array_shift($key_parts) == 'tid')) {
        $term_id = reset($key_parts);
        $reference_term_ids[$term_id] = end($key_parts);
      }
    }
    // Reorder the terms per $sorted_term_ids.
    $sorted_term_fields = [];
    foreach ($sorted_term_ids as $term_id) {
      if (isset($reference_term_ids[$term_id])) {
        $key = "tid:{$term_id}:{$reference_term_ids[$term_id]}";
        if (isset($form['terms'][$key])) {
          $sorted_term_fields[$key] = $form['terms'][$key];
          unset($form['terms'][$key]);
          if (isset($sorted_term_fields[$key]['weight'])) {
            $sorted_term_fields[$key]['weight']['#access'] = FALSE;
          }
        }
      }
    }
    $form['terms'] = array_merge($form['terms'], $sorted_term_fields);
    // Remove the “reset to alphabetical” action.
    if (isset($form['actions']) && (isset($form['actions']['reset_alphabetical']))) {
      $form['actions']['reset_alphabetical']['#access'] = FALSE;
    }
    // Hide the “Weight” table header.
    if (isset($form['terms']['#header']) && (isset($form['terms']['#header']['weight']))) {
      $form['terms']['#header']['weight'] = [];
    }
    // Replace the help text, because terms can not be reordered.
    if (isset($form['help']) && (isset($form['help']['message'])) && (isset($form['help']['message']['#markup']))) {
      $form['help']['message']['#markup'] = t('Terms in this vocabulary are automatically sorted by date and can not be manually reordered.');
    }
    // Removing tabledrag altogether causes “core/modules/taxonomy/taxonomy.js”
    // to trigger JavaScript errors that can't be surpressed by any reasonable
    // means (at least not when asset aggregation is enabled so that the
    // offending file can't be unattached), so instead tabledrag-related
    // elements are hidden using CSS: attach the library and add an attribute to
    // the form for the CSS selectors.
    $form['#attached'] ??= [];
    $form['#attached']['library'] ??= [];
    $form['#attached']['library'][] = 'MODULE/LIBRARY';
    $form['#attributes'] ??= [];
    $form['#attributes']['data-MODULE-tabledrag-disabled'] = 1;
  }
}

Note that the term list forms all appear to have the same $form_id, hence the reference to the helper function that uses the route to check that the list being altered is the right one:

/**
 * If the current route is a vocabulary overview, return the vocabulary ID.
 */
function _MODULE_vocabulary_id_from_overview_route() {
  $taxonomy_vocabulary_id = NULL;
  if (($route = \Drupal::routeMatch()) && ($route_name = $route->getRouteName()) && ($route_name = 'entity.taxonomy_vocabulary.overview_form') && ($taxonomy_vocabulary = $route->getParameter('taxonomy_vocabulary'))) {
    $taxonomy_vocabulary_id = $taxonomy_vocabulary->id();
  }
  return $taxonomy_vocabulary_id;
}

There's an additional helper function to retrieve the terms in the preferred order—this could be omitted if the terms should be sorted alphabetically, but in that case it might make sense to add a submit handler (or something) that ensures that the term weights are always 0, because the weight still exists, it's just not manageable. In my case, though, I want terms sorted by their date field, so:

/**
 * Return sessions sorted by date.
 */
function _MODULE_sorted_term_ids() {
  $term_ids = \Drupal::entityQuery('taxonomy_term')
    ->accessCheck(TRUE)
    ->condition('vid', 'session')
    ->sort('field_date', 'ASC')
    ->sort('field_date.end_value', 'ASC')
    ->execute();
  return $term_ids;
}

Finally, because the terms can not be manually sorted, the tabledrag elements on the term list are superfluous (and potentially confusing). A library was attached and an attribute was added to the affected form above in MODULE_form_taxonomy_overview_terms_alter(), some CSS hides the irrelevant elements:

form[data-MODULE-tabledrag-disabled='1'] .tabledrag-toggle-weight-wrapper,
form[data-MODULE-tabledrag-disabled='1'] a.tabledrag-handle {
  display: none;
}
Ä’äy Chù (Slim's River) West trail in Kluane National Park

Ä’äy Chù (Slim's River) West trail in Kluane National Park, August 2019