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 tabledrag.
    if (isset($form['terms']['#tabledrag'])) {
      unset($form['terms']['#tabledrag']);
    }
    // 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.');
    }
  }
}

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')
    ->condition('vid', 'session')
    ->sort('field_date', 'ASC')
    ->sort('field_date.end_value', 'ASC')
    ->execute();
  return $term_ids;
}

One last thing: I was seeing an error in the JavaScript console because the taxonomy module evidently expects tabledrag to be available. The offending file can be unlinked via hook_js_alter():

/**
 * Implements hook_js_alter().
 *
 * With tabledrag removed, the taxonomy JS throws a dependency error, so remove
 * it.
 */
function MODULE_js_alter(&$javascript) {
  if (($taxonomy_vocabulary_id = _MODULE_vocabulary_id_from_overview_route()) && ($taxonomy_vocabulary_id == 'session') && (isset($javascript['core/modules/taxonomy/taxonomy.js']))) {
    unset($javascript['core/modules/taxonomy/taxonomy.js']);
  }
}

Note that the _MODULE_vocabulary_id_from_overview_route() helper function is re-used to ensure that the right term list is impacted.

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

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