The title might be a little confusing but couldn't think of anything else atm.

I've got a use case that needs views exposed filters (hierarchical taxonomy terms) structured like the "Nested Checkboxes/Radio Buttons" but with a twist:
Instead of checkboxes or radio buttons I need links.
Sort of like a "Nested Links" display. (hence the title of this issue)

We've got "Nested Checkboxes/Radio Buttons", we've got "Links", why not combine them into "Nested Links" ? From what I understand it wouldn't be too hard to implement and theming-wise it would make some things a lot easier!

over-simplified, having an output like

<ul>
  <li><a href=#>term 1</a></li>
  <li><a href=#>term 2</a>
    <ul>
      <li><a href=#>sub-term</a></li>
    </li>
  </li>
  <li><a href=#>term 3</a></li>
</ul>

would mean easier to theme hierarchical exposed filters, plus the ability to use a few js tricks to make this prettier and more functional.

Comments

DanielWashbrook’s picture

I was in the same situation. To save a bit of time, I copied the theme function code in better_exposed_filters.theme function theme_select_as_tree for counting depth

<?php
preg_match('/^(-*).*$/', $option_label, $matches);
$depth = strlen($matches[1]);
?>

And then wrapped a div tag around the link with a depth class

<?php
$link = '<div class="depth-'.$depth.'">'.l($value, bef_replace_query_string_arg($name, $key, $multiple));.'</div>';
?>

and then I can style the different depth levels that I need. Not a great solution, but it works.

Here's the whole thing to go into your template.php file:

<?php
function YOURTHEME_select_as_links($vars) {
  $element = $vars['element'];

  $output = '';
  $name = $element['#name'];

  // Collect selected values so we can properly style the links later
  $selected_options = array();
  if (empty($element['#value'])) {
    if (!empty($element['#default_values'])) {
      $selected_options[] = $element['#default_values'];
    }
  }
  else {
    $selected_options[] = $element['#value'];
  }

  // Add to the selected options specified by Views whatever options are in the
  // URL query string, but only for this filter
  $urllist = parse_url(request_uri());
  if (isset($urllist['query'])) {
    $query = array();
    parse_str(urldecode($urllist['query']), $query);
    foreach ($query as $key => $value) {
      if ($key != $name) {
        continue;
      }
      if (is_array($value)) {
        // This filter allows multiple selections, so put each one on the selected_options array
        foreach ($value as $option) {
          $selected_options[] = $option;
        }
      }
      else {
        $selected_options[] = $value;
      }
    }
  }

  $curr_depth = 0;
  $element['#options']['All'] = t('View All');
  // Go through each filter option and build the appropriate link or plain text
  foreach ($element['#options'] as $option => $elem) {
    
    // Check for Taxonomy-based filters
    if (is_object($elem)) {
      list($option, $elem) = each(array_slice($elem->option, 0, 1, TRUE));
    }

    /*
     * Check for optgroups.  Put subelements in the $element_set array and add a group heading.
     * Otherwise, just add the element to the set
     */
    $element_set = array();
    if (is_array($elem)) {
      $element_set = $elem;
    }
    else {
      $element_set[$option] = $elem;
    }

    $links = array();
    $multiple = !empty($element['#multiple']);
    
             
    $html = '';
    foreach ($element_set as $key => $value) {
      //dpm($value);
      preg_match('/^(-*).*$/', $value, $matches);
      $depth = strlen($matches[1]);
      //dpm($depth);

      
      // Custom ID for each link based on the <select>'s original ID
      $id = drupal_html_id($element['#id'] . '-' . $key);
      if (array_search($key, $selected_options) === FALSE) {
        $link = l(ltrim($value, '-'), bef_replace_query_string_arg($name, $key, $multiple));
        $html = '<div class="depth-'.$depth.'">'.$link.'</div>';
        $output .= theme('form_element', array('element' => array( '#id' => $id, '#children' => $html, '#markup' => '',
          '#name' => 'depth-'.$depth,)));
      } else {
        // Selected value is output without a link
        // TODO: add link to remove this option from the filter?
        $elem = array(
          '#id' => $id,
          '#children' => $value,
          '#markup' => '',
          '#name' => 'depth-'.$depth,
          );
        _form_set_class($elem, array('bef-select-as-links-selected'));
        $output .= theme('form_element', array('element' => $elem));
      }
    } // end foreach
    

  } // end foreach 



  $properties = array(
    '#description' => isset($element['#description']) ? $element['#description'] : '',
    '#children' => $output,
    '#name' => 'test-list',
  );

  return '<div class="bef-select-as-links">'
    . theme('form_element', array('element' => $properties))
    . '</div>';
}
?>

Daniel

aristeides’s picture

Wow, thank a lot! that actually works!!

hanskuiters’s picture

Status: Closed (fixed) » Active

Bit of an old threat, but here is my tweak for a real nested links list.

// removed

Edit: I removed the code from this post because of bugs. See post #5.

openhairpinleft’s picture

Capono, you rock! Thanks a lot for this solution, it's excellent.
I know it's easy to make suggestions, but maybe you could consider to contribute the code to the BEF or Views itself. Drupal needs this facility as an output plugin.
Cheers

hanskuiters’s picture

Status: Active » Needs review

Thanks. There were some bugs in post #3, so here is a better one. Put this code in your temple.php file in your theme folder and change the function name to your theme name.

function YOURTHEME_select_as_links($vars) {
  $element = $vars['element'];

  $output = '<ul class="bef-tree">';
  $curr_depth = 0;
  $name = $element['#name'];

  // Collect selected values so we can properly style the links later.
  $selected_options = array();
  $parents = array();
  if (empty($element['#value'])) {
    if (!empty($element['#default_values'])) {
      $selected_options[] = $element['#default_values'];
    }
  }
  else {
    $selected_options[] = $element['#value'];
    $parents[] = array_keys(taxonomy_get_parents($element['#value']));
  }

  // Add to the selected options specified by Views whatever options are in the
  // URL query string, but only for this filter.
  $urllist = parse_url(request_uri());
  if (isset($urllist['query'])) {
    $query = array();
    parse_str(urldecode($urllist['query']), $query);
    foreach ($query as $key => $value) {
      if ($key != $name) {
        continue;
      }
      if (is_array($value)) {
        // This filter allows multiple selections, so put each one on the
        // selected_options array.
        foreach ($value as $option) {
          $selected_options[] = $option;
        }
      }
      else {
        $selected_options[] = $value;
      }
    }
  }

  // Clean incoming values to prevent XSS attacks.
  if (is_array($element['#value'])) {
    foreach ($element['#value'] as $index => $item) {
      unset($element['#value'][$index]);
      $element['#value'][filter_xss($index)] = filter_xss($item);
    }
  }
  elseif (is_string($element['#value'])) {
    $element['#value'] = filter_xss($element['#value']);
  }

  // Go through each filter option and build the appropriate link or plain text.
  foreach ($element['#options'] as $option => $elem) {
    // Check for Taxonomy-based filters.
    if (is_object($elem)) {
      $slice = array_slice($elem->option, 0, 1, TRUE);
      list($option, $elem) = each($slice);
    }

    // Check for optgroups.  Put subelements in the $element_set array and add
    // a group heading. Otherwise, just add the element to the set.
    $element_set = array();
    if (is_array($elem)) {
      $element_set = $elem;
    }
    else {
      $element_set[$option] = $elem;
    }

    $links = array();
    $html = '';
    $multiple = !empty($element['#multiple']);

    // If we're in an exposed block, we'll get passed a path to use for the
    // Views results page.
    $path = '';
    if (!empty($element['#bef_path'])) {
      $path = $element['#bef_path'];
    }

    foreach ($element_set as $key => $value) {
      if (t('- Any -') == $value) {
        $depth = 0;
      }
      else {
        preg_match('/^(-*).*$/', $value, $matches);
        $depth = strlen($matches[1]);
        $value = ltrim($value, '-');
      }
      // Custom ID for each link based on the <select>'s original ID.
      $id = drupal_html_id($element['#id'] . '-' . $key);
      $elem = array(
        '#id' => $id,
        '#markup' => '',
        '#type' => 'bef-link',
        '#name' => $id,
      );

      $active = NULL;
      if (array_search($key, $selected_options) === FALSE) {
        $elem['#children'] = l($value, bef_replace_query_string_arg($name, $key, $multiple, FALSE, $path));
        $html .= theme('form_element', array('element' => $elem));
        $selected = '';
        if (in_array($key, $parents[0]) == TRUE) {
          $active = ' active';
        }
      }
      else {
        $elem['#children'] = l($value, bef_replace_query_string_arg($name, $key, $multiple, TRUE, $path));
        _form_set_class($elem, array('bef-select-as-links-selected'));
        $html .= str_replace('form-item', 'form-item selected', theme('form_element', array('element' => $elem)));
        $selected = ' selected';
        $active = NULL;
      }
    }

    if ($depth > $curr_depth) {
      // We've moved down a level: create a new nested <ul>.
      // TODO: Is there is a way to jump more than one level deeper at a time?
      // I don't think so...
      $output .= "<ul class='bef-tree-child bef-tree-depth-$depth". $selected ."'><li class='depth-". $depth ." item-". $key ."'>$html";
      $curr_depth = $depth;
    }
    elseif ($depth < $curr_depth) {
      // We've moved up a level: finish previous <ul> and <li> tags, once for
      // each level, since we can jump multiple levels up at a time.
      while ($depth < $curr_depth) {
        $output .= '</li></ul>';
        $curr_depth--;
      }
      $output .= "</li><li class='depth-". $depth ." item-". $key . $active . $selected ."'>$html";
    }
    else {
      // Remain at same level as previous entry. No </li> needed if we're at
      // the top level.
      if (0 == $curr_depth) {
        $output .= "<li class='depth-". $depth ." item-". $key . $active . $selected ."'>$html";
      }
      else {
        $output .= "</li><li class='depth-". $depth ." item-". $key ."'>$html";
      }
    }
  }

  if (!$curr_depth) {
    // Close last <li> tag.
    $output .= '</li>';
  }
  else {
    // Finish closing <ul> and <li> tags.
    while ($curr_depth) {
      $curr_depth--;
      $output .= '</li></ul></li>';
    }
  }

  // Close the opening <ul class="bef-tree"> tag.
  $output .= '</ul>';


  /*$properties = array(
    '#description' => isset($element['#bef_description']) ? $element['#bef_description'] : '',
    '#children' => $output,
  );*/

  $output .= '<div class="bef-select-as-links">';
  //$output .= theme('form_element', array('element' => $properties));
  if (!empty($element['#value'])) {
    if (is_array($element['#value'])) {
      foreach ($element['#value'] as $value) {
        $output .= '<input type="hidden" name="' . $name . '[]" value="' . $value . '" />';
      }
    }
    else {
      $output .= '<input type="hidden" name="' . $name . '" value="' . $element['#value'] . '" />';
    }
  }
  $output .= '</div>';

  return $output;
}
mikeker’s picture

Status: Active » Closed (fixed)

It sounds like folks have found a solution for this in the theme layer. If anyone can post a patch that turns this into a BEF option, I'd be happy to review and commit it. Until then, I'm going to mark this as closed. I've linked to this issue from the BEF docs so hopefully folks will find and continue to contribute to it.

Thank you to DanielWashbrook and capono for posting your solutions!

amirtaiar’s picture

Status: Needs review » Closed (fixed)

Thank you, I just need this option on a website I develop.
I didn't really understand the flow -
- I have put this code in my templete.php file and change the YOURTHEME to the name of my theme.
- Create a views page with 2 exposed fllters [Terms fields] that display in a block.

Yet I don't see any change in the html.

jbfelix’s picture

Go to admin/config/development/performance and delete the cache.