Creating a "collapsible" Views Exposed Filter

When building sites for our customers we usually create some administrative views (like for content or user administration) to make it easier for editors to work with the site. For a little more user experience we modify these views (especially the exposed form providing the filters). One of these modifications is to create a "collapsible" filter dropdown.

The Mission

Think of a content administration view with filters for content type, status, author and so on. Normally the filter for the content type allows multiple selections and would look similar to the one in the image.

But we want the filter to act as a single dropdown that could be expanded to a multi-select list if the user wants to filter for several types. 

The Solution

To achieve this we need a small custom module and alter the exposed form:

<?php 
/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Alter views exposed forms for collapsible filters.
 */
function MYMODULE_form_views_exposed_form_alter(&$form, &$form_state) {
  if (empty($form_state['view']) || !in_array($form_state['view']->name, array('NAME_OF_VIEW', 'NAME_OF_VIEWS_DISPLAY'))) {
    // We alter the exposed form of a single views display, so return if this is
    // not the expected view.
    return;
  }

  if (isset($form['type'])) {
    // Add option to select all items (equals to resetting the filter).
    $options = array(
      'All' => variable_get('views_exposed_filter_any_label', 'new_any') == 'old_any' ? t('<Any>') : t('- Any -'),
    );
    $options += $form['type']['#options'];
    // Change size of field based on number of options (max: 5 items).
    $form['type']['#size'] = min(array(count($options), 5));

    if (count($options) <= 2) {
      // Hide filter if there is only one option available (additional 
      // to "All").
      $form['type']['#access'] = FALSE;
    }
    $form['type']['#options'] = $options;
  }
  

  // Alter multi-value dropdowns.
  $form_multiple_selects = array();
  foreach (element_children($form) as $element_name) {
    if (isset($form[$element_name]['#type']) && $form[$element_name]['#type'] == 'select' && !empty($form[$element_name]['#multiple'])) {
      $form_multiple_selects[$element_name] = array(
        'size' => isset($form[$element_name]['#size']) ? $form[$element_name]['#size'] : 5,
      );
    }
  }
  if (count($form_multiple_selects)) {
    $form['#attached'] += array(
      'js' => array(), 
      'css' => array(),
    );
    // Attach custom javascript to the form.
    $form['#attached']['js'][] = drupal_get_path('module', 'MYMODULE') . '/js/MYMODULE.admin.js';
    $form['#attached']['js'][] = array(
      'data' => array(
        'collapsibleFilter' => array(
          'multiple_selects' => $form_multiple_selects,
        ),
      ),
      'type' => 'setting',
    );
  }
}
?>

Unfortunately we have to do some more magic to avoid errors after selecting the new option "All". Because we manually added the option in the form-alter, Views does not know about it and would throw an error after selecting it. The simplest way to avoid it, is to remove the filter value before displaying the results:

<?php 
/**
 * Implements hook_views_pre_view().
 */
function MYMODULE_views_pre_view(&$view, &$display_id, &$args) {
  if (!in_array($view->name, array('NAME_OF_VIEW', 'NAME_OF_VIEWS_DISPLAY'))) {
    return;
  }
  foreach (array('type') as $filter) {
    if (!empty($_GET[$filter]) && (is_array($_GET[$filter])) && reset($_GET[$filter]) == 'All') {
      // Remove the filter value because it is manually added and thus 
      // unknown to Views.
      unset($_GET[$filter]);
    }
  }
}
?>

Finally we add our JavaScript to append the "plus sign" to the dropdown and add the "collapse" functionality:

(function($) {

  /**
   * Change multi-value dropdown to single-value dropdown and back (visually).
   */
  Drupal.behaviors.collapsibleFilterRewriteMultipleSelect = {
    attach: function(context, settings) {
      $.each(Drupal.settings.collapsibleFilter.multiple_selects, function(name, settings) {
        $('select#edit-' + name)
              .once('collapsible-filter-multiple-rewrite')
              .each(function() {

          var selectionCount = $('option:selected', $(this)).length;
          if (selectionCount <= 1) {
            // Set size of select to 1 if there is not more than 1 selected.
            $(this).attr('size', 1);
            // Remove attribute "multiple".
            $(this).removeAttr('multiple');
            // Set default option.
            if (selectionCount === 0 || $(this).val() === 'All') {
              $(this).val('All');
            }

            // Add link to expand the dropdown.
            $expand = $('<a>')
                    .addClass('select-expander')
                    .attr('href', '#')
                    .attr('title', Drupal.t('Expand selection'))
                    .html('[+]')
                    .click(function() {
                      // Get corresponding select element.
                      $select = $(this)
                              .parent('.form-type-select')
                              .find('.collapsible-filter-multiple-rewrite-processed');
                      // Expand element.
                      $select.attr('size', settings.size)
                              .attr('multiple', 'multiple');
                      $(this).remove();
                    })
                    .appendTo($(this).parent());
          }
        });
      });
    }
  };

})(jQuery);

Result

After you have all this together, users will be able to choose whether to select a single type from a dropdown or multiple values from a list. If a user selected multiple values the filter automatically displays as select list. 

 

Comments

ashiwebi's picture
ashiwebi

This is what I exactly I need. Thanks

Add new comment