Skip to main content

Creating a table with draggable weights in Drupal 7

Ki H. Kim
Director, Engineering

Drupal 7 has built-in support for draggable tables that enables users to order rows by dragging the cross hair icon up and down. You can see one example at the admin setting for text input format (path: /admin/config/content/formats). Draggable tables drupal_add_tabledrag() function is the secret to building draggable tables. Here is how to do it. This example creates an admin settings page to let a site admin pick the list of contents to be used for a front page slideshow. The implementation of a slideshow itself is not demonstrated here. We are building a custom module called 'slideshow' and the code shown here is going to be in slideshow.module file. The table consists of 5 columns: * The first is for the cross hair icon. It can have its own column or be embedded into another column as you can see from the two images. * 2nd is for a textbox that suggests contents as you type using the autocomplete feature. * 3rd is a regular textbox for the slideshow caption. * 4th is a weights column that is invisible. It will be shown when the user clicks on "Show row weights". * The last column is an operation column for "Remove" links. Draggable tables

Define menu item and the page callback, and register theme

<?php
// Implements hook_menu().
function slideshow_menu() {
  $items = array();
  
  // Slideshow manage page.
  $items['admin/config/user-interface/slideshow'] = array(
    'title' => 'Slideshow',
    'description' => 'Manage slideshow list.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('slideshow_manage'),
    'access arguments' => array('administer content'),
  );
  
  // Autocomplete callback.
  $items['slideshow/autocomplete'] = array(
    'page callback' => 'slideshow_autocomplete',
    'access arguments' => array('administer content'),
    'type' => MENU_CALLBACK,
  );
  
  return $items;
}
// Implements hook_theme().
function slideshow_theme($existing, $type, $theme, $path) {
  return array(
    'slideshow_manage' => array(
      'render element' => 'form',
    ),
  );
}
?>

Implement callback function for the slideshow manage page

This is the heart of the feature along with its theme function.

<?php
function slideshow_manage() {
  $slides = variable_get('slideshow_slides', array());
  
  // Empty row for new slide input.
  $slides[] = array('nid' => 0, 'caption' => '', 'weight' => 0);
  
  $form['#tree'] = TRUE;
  foreach ($slides as $id => $slide) {
    if (!empty($slide['nid'])) {
      $node = node_load($slide['nid']);
    }
    else {
      $node = (object)array('title' => '');
    }
    
    // Textfield to hold content id.
    $form['slides'][$slide['nid']]['node'] = array(
      '#type' => 'textfield',
      '#autocomplete_path' => 'ui-slideshow/autocomplete',
      '#default_value' => check_plain($node->title) . (!empty($node->nid) ? " [$node->nid]" : ''),
    );
    // Caption for the slideshow.
    $form['slides'][$slide['nid']]['caption'] = array(
      '#type' => 'textfield',
      '#default_value' => $slide['caption'],
    );
    // This field is invisible, but contains sort info (weights).
    $form['slides'][$slide['nid']]['weight'] = array(
      '#type' => 'weight',
      '#title' => t('Weight'),
      '#title_display' => 'invisible',
      '#default_value' => $slide['weight'],
    );
    // Operation links (to remove rows).
    $form['slides'][$slide['nid']]['op'] = array(
      '#markup' => '<a href="#" class="remove-row">' . t('Remove') . '</a>',
    );
  }
  
  // jQuery to implement remove feature. Setting the text field to empty
  // and submitting the form will remove the rows.
  $js = <<<EOD
(function ($) {
  $(function() {
    $('a.remove-row').click(function() {
      $(this).closest('tr').fadeOut(function() {
        $(this).find('input.form-autocomplete').val('')
          .closest('form').submit();
      });
    });;
  });
})(jQuery);
EOD;
  
  drupal_add_js($js, array('type' => 'inline'));
  
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save changes'));
  
  return $form;
}
?>

Build submit handler for the form

<?php
// This looks for the node id in the submitted value, "Test title string [123]"
function slideshow_manage_submit($form, &$form_state) {
  $slides = array();
  foreach ($form_state['values']['slides'] as $slide) {
    preg_match('/\[(\d+)\]$/', $slide['node'], $matches);
    if ($nid = !empty($matches[1]) ? (int)$matches[1] : 0) {
      $slides[] = array(
        'nid' => $nid,
        'caption' => $slide['caption'],
        'weight' => $slide['weight'],
      );
    }
  }
  
  if (!empty($slides)) {
    usort($slides, '_slideshow_arraysort');
  }
  
  variable_set('slideshow_slides', $slides);
  drupal_set_message(t('Slides have been saved.'));
}
// Custom array sort function by weight.
function _slideshow_arraysort($a, $b) {
  if (isset($a['weight']) && isset($b['weight'])) {
    return $a['weight'] < $b['weight'] ? -1 : 1;
  }
  return 0;
}
?>

Implement callback for autocomplete

<?php
// Search titles of article and page contents.
function slideshow_autocomplete($string) {
  $query = db_select('node', 'n');
  $query->fields('n', array('nid', 'title'));
  
  $types = array('article', 'page'); // Add additional content types as you like.
  if (!empty($types)) {
    $db_or = db_or();
    foreach ($types as $type) {
      if (!empty($type)) {
        $db_or->condition('n.type', $type, '=');
      }
    }
    $query->condition($db_or);
  }
  
  $result = $query
    ->condition('n.title', '%' . db_like($string) . '%', 'LIKE')
    ->range(0, 10)
    ->execute();
  
  $matches = array();
  foreach ($result as $row) {
    $matches[$row->title . " [$row->nid]"] = check_plain($row->title) . " [$row->nid]";
  }
  
  drupal_json_output($matches);
}
?>

Build theme function for the form

<?php
// Theme function for slideshow_manage().
function theme_slideshow_manage($variables) {
  $form = $variables['form'];
  
  $rows = array();
  foreach (element_children($form['slides']) as $nid) {
    $form['slides'][$nid]['weight']['#attributes']['class'] = array('slides-order-weight');
    $rows[] = array(
      'data' => array(
        array('class' => array('slide-cross')),
        drupal_render($form['slides'][$nid]['node']),
        drupal_render($form['slides'][$nid]['caption']),
        drupal_render($form['slides'][$nid]['weight']),
        drupal_render($form['slides'][$nid]['op']),
      ),
      'class' => array('draggable'),
    );
  }
  
  $header = array('', t('Content'), t('Caption (If empty, title is used)'), t('Weight'), t('Operations'));
  $output = drupal_render($form['note']);
  $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'slides-order')));
  $output .= drupal_render_children($form);
  
  drupal_add_tabledrag('slides-order', 'order', 'sibling', 'slides-order-weight');
  
  return $output;
}
?>

Functions slideshow_manage() and theme_slideshow_manage() are notable as the driver for the draggable table.