You are here

tablefield.module in TableField 7

Same filename and directory in other branches
  1. 6 tablefield.module
  2. 7.3 tablefield.module
  3. 7.2 tablefield.module

This module provides a set of fields that can be used to store tabular data with a node. The implementation uses a custom CCK widget.

File

tablefield.module
View source
<?php

/**
 * @file
 * This module provides a set of fields that can be used to store
 * tabular data with a node. The implementation uses a custom CCK widget.
 */

/**
 * @todo should we create a helper function for sanitization?
 *  - we should see if it makes sense to sanitize on load as well as view
 */

/**
 * Implements hook_field_info().
 */
function tablefield_field_info() {
  return array(
    'tablefield' => array(
      'label' => t('Table Field'),
      'description' => t('Stores a table of text fields'),
      'default_widget' => 'tablefield',
      'default_formatter' => 'default',
    ),
  );
}

/**
 * Implements hook_field_settings_form().
 */
function tablefield_field_settings_form($field, $instance, $has_data) {
  $form = array();
  $form['lock_values'] = array(
    '#type' => 'checkbox',
    '#title' => 'Lock table field defaults from further edits during node add/edit.',
    '#default_value' => isset($field['settings']['lock_values']) ? $field['settings']['lock_values'] : FALSE,
  );
  $form['cell_processing'] = array(
    '#type' => 'radios',
    '#title' => t('Table cell processing'),
    '#default_value' => isset($field['settings']['cell_processing']) ? $field['settings']['cell_processing'] : 0,
    '#options' => array(
      t('Plain text'),
      t('Filtered text (user selects input format)'),
    ),
  );
  $form['default_message'] = array(
    '#type' => 'markup',
    '#value' => t('To specify a default table, use the &quot;Default Value&quot; above. There you can specify a default number of rows/columns and values.'),
  );
  return $form;
}

/**
 * Implements hook_field_schema().
 */
function tablefield_field_schema($field) {
  return array(
    'columns' => array(
      'value' => array(
        'type' => 'text',
        'size' => 'big',
      ),
      'format' => array(
        'type' => 'varchar',
        'length' => 255,
        'default value' => '',
      ),
    ),
  );
}

/**
 * Implements hook_field_presave().
 */
function tablefield_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $delta => $table) {
    if (empty($table['value'])) {
      $tablefield = array();
      foreach ($table['tablefield'] as $key => $value) {
        $tablefield[$key] = $value;
      }
      $items[$delta]['value'] = serialize($tablefield);
    }
    elseif (empty($table['tablefield'])) {

      // Batch processing only provides the 'value'
      $items[$delta]['tablefield'] = unserialize($items[$delta]['value']);
    }
  }
}

/**
 * Implements hook_field_validate().
 */
function tablefield_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  drupal_add_css(drupal_get_path('module', 'tablefield') . '/tablefield.css');

  // Catch empty form submissions for required tablefields
  if ($instance['required'] && isset($items[0]) && tablefield_field_is_empty($items[0], $field)) {
    $message = t('@field is a required field.', array(
      '@field' => $instance['label'],
    ));
    $errors[$field['field_name']][$langcode][0]['tablefield'][] = array(
      'error' => 'empty_required_tablefield',
      'message' => $message,
    );
  }
}

/**
 * Implements hook_field_widget_error().
 */
function tablefield_field_widget_error($element, $error, $form, &$form_state) {
  form_error($element['tablefield'], $error[0]['message']);
}

/**
 * Implements hook_field_load().
 */
function tablefield_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  foreach ($items as $delta => $table) {
    if (isset($table[0]['value'])) {
      $items[$delta][0]['tabledata'] = tablefield_rationalize_table(unserialize($table[0]['value']));
    }
  }
}

/**
 * Implementation of hook_field_is_empty().
 */
function tablefield_field_is_empty($item, $field) {

  // @todo, is this the best way to mark the default value form?
  // if we don't, it won't save the number of rows/cols
  // Allow the system settings form to have an emtpy table
  $arg0 = arg(0);
  if ($arg0 == 'admin') {
    return FALSE;
  }

  // Remove the preference fields to see if the table cells are all empty
  unset($item['tablefield']['rebuild']);
  unset($item['tablefield']['import']);
  if (!empty($item['tablefield'])) {
    foreach ($item['tablefield'] as $cell) {
      if (!empty($cell)) {
        return FALSE;
      }
    }
  }
  return TRUE;
}

/**
 * Implementation of hook_field_formatter_info().
 */
function tablefield_field_formatter_info() {
  return array(
    'default' => array(
      'label' => t('Tabular View'),
      'field types' => array(
        'tablefield',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function tablefield_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
  $settings = $display['settings'];
  $formatter = $display['type'];
  foreach ($items as $delta => $table) {

    // Rationalize the stored data
    if (!empty($table['tablefield'])) {
      $tabledata = tablefield_rationalize_table($table['tablefield']);
    }
    elseif (!empty($table['value'])) {
      $tabledata = tablefield_rationalize_table(unserialize($table['value']));
    }

    // Run the table through input filters
    if (isset($tabledata)) {
      if (!empty($tabledata)) {
        foreach ($tabledata as $row_key => $row) {
          foreach ($row as $col_key => $cell) {
            if (!empty($table['format'])) {
              $tabledata[$row_key][$col_key] = array(
                'data' => check_markup($cell, $table['format']),
                'class' => array(
                  'row_' . $row_key,
                  'col_' . $col_key,
                ),
              );
            }
            else {
              $tabledata[$row_key][$col_key] = array(
                'data' => check_plain($cell),
                'class' => array(
                  'row_' . $row_key,
                  'col_' . $col_key,
                ),
              );
            }
          }
        }
      }

      // Pull the header for theming
      $header = array_shift($tabledata);

      // Theme the table for display
      $element[$delta]['#markup'] = theme('tablefield_view', array(
        'header' => $header,
        'rows' => $tabledata,
        'delta' => $delta,
      ));
    }
  }
  return $element;
}

/**
 * Implements hook_field_widget_info().
 */
function tablefield_field_widget_info() {
  return array(
    'tablefield' => array(
      'label' => t('Table field'),
      'field types' => array(
        'tablefield',
      ),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
        'default value' => FIELD_BEHAVIOR_DEFAULT,
      ),
    ),
  );
}

/**
 * Implements hook_widget_form().
 */
function tablefield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  drupal_add_css(drupal_get_path('module', 'tablefield') . '/tablefield.css');
  $element['#type'] = 'tablefield';
  $form['#attributes']['enctype'] = 'multipart/form-data';

  // Establish a list of saved/submitted/default values
  if (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta) {

    // Rebuilding table rows/cols
    $default_value = tablefield_rationalize_table($form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']);
  }
  elseif (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield-import-button-' . $field['field_name'] . '-' . $delta) {

    // Importing CSV data
    tablefield_import_csv($form, $form_state);
    $default_value = tablefield_rationalize_table($form_state['input'][$field['field_name']][$langcode][$delta]['tablefield']);
  }
  elseif ($form_state['submitted'] && isset($items[$delta]) && isset($items[$delta]['tablefield'])) {

    // A form was submitted
    $default_value = tablefield_rationalize_table($items[$delta]['tablefield']);
  }
  elseif (isset($items[$delta]['value'])) {

    // Default from database (saved node)
    $default_value = tablefield_rationalize_table(unserialize($items[$delta]['value']));
  }
  else {

    // After the first table, we don't want to populate the values in the table
    if ($delta > 0) {
      tablefield_delete_table_values($instance['default_value'][0]['tablefield']);
    }

    // Get the widget default value
    $default_value = tablefield_rationalize_table($instance['default_value'][0]['tablefield']);
    $default_count_cols = isset($items[0]['tablefield']['rebuild']['count_cols']) ? $items[0]['tablefield']['rebuild']['count_cols'] : 5;
    $default_count_rows = isset($items[0]['tablefield']['rebuild']['count_cols']) ? $items[0]['tablefield']['rebuild']['count_cols'] : 5;
  }
  if (!empty($instance['description'])) {
    $help_text = $instance['description'];
  }
  else {
    $help_text = t('The first row will appear as the table header. Leave the first row blank if you do not need a header.');
  }
  $element['tablefield'] = array(
    '#title' => $element['#title'],
    '#description' => $help_text,
    '#attributes' => array(
      'id' => 'form-tablefield-' . $field['field_name'] . '-' . $delta,
      'class' => array(
        'form-tablefield',
      ),
    ),
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#collapsible' => FALSE,
    '#prefix' => '<div id="tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper">',
    '#suffix' => '</div>',
  );

  // Give the fieldset the appropriate class if it is required
  if ($element['#required']) {
    $element['tablefield']['#title'] .= ' <span class="form-required" title="' . t('This field is required') . '">*</span>';
  }
  $arg0 = arg(0);
  if ($arg0 == 'admin') {
    $element['tablefield']['#description'] = t('This form defines the table field defaults, but the number of rows/columns and content can be overridden on a per-node basis. The first row will appear as the table header. Leave the first row bland if you do not need a header.');
  }

  // Determine how many rows/columns are saved in the data
  if (!empty($default_value)) {
    $count_rows = count($default_value);
    $count_cols = 0;
    foreach ($default_value as $row) {
      $temp_count = count($row);
      if ($temp_count > $count_cols) {
        $count_cols = $temp_count;
      }
    }
  }
  else {
    $count_rows = count($default_value);
    $count_cols = isset($default_count_cols) ? $default_count_cols : 0;
    $count_rows = isset($default_count_rows) ? $default_count_rows : 0;
  }

  // Override the number of rows/columns if the user rebuilds the form.
  if (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta) {
    $count_cols = $form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']['rebuild']['count_cols'];
    $count_rows = $form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']['rebuild']['count_rows'];
    drupal_set_message(t('Table structure rebuilt.'), 'status', FALSE);
  }

  // Render the form table
  $element['tablefield']['a_break'] = array(
    '#markup' => '<table>',
  );
  for ($i = 0; $i < $count_rows; $i++) {
    $zebra = $i % 2 == 0 ? 'even' : 'odd';
    $element['tablefield']['b_break' . $i] = array(
      '#markup' => '<tr class="tablefield-row-' . $i . ' ' . $zebra . '">',
    );
    for ($ii = 0; $ii < $count_cols; $ii++) {
      $instance_default = isset($instance['default_value'][$delta]) ? $instance['default_value'][$delta]['tablefield']["cell_{$i}_{$ii}"] : array();
      if (!empty($instance_default) && !empty($field['settings']['lock_values']) && $arg0 != 'admin') {

        // The value still needs to be send on every load in order for the
        // table to be saved correctly.
        $element['tablefield']['cell_' . $i . '_' . $ii] = array(
          '#type' => 'value',
          '#value' => $instance_default,
        );

        // Display the default value, since it's not editable.
        $element['tablefield']['cell_' . $i . '_' . $ii . '_display'] = array(
          '#type' => 'item',
          '#title' => $instance_default,
          '#prefix' => '<td style="width:' . floor(100 / $count_cols) . '%">',
          '#suffix' => '</td>',
        );
      }
      else {
        $cell_default = isset($default_value[$i][$ii]) ? $default_value[$i][$ii] : '';
        $element['tablefield']['cell_' . $i . '_' . $ii] = array(
          '#type' => 'textfield',
          '#maxlength' => 2048,
          '#size' => 0,
          '#attributes' => array(
            'id' => 'tablefield_' . $delta . '_cell_' . $i . '_' . $ii,
            'class' => array(
              'tablefield-row-' . $i,
              'tablefield-col-' . $ii,
            ),
            'style' => 'width:100%',
          ),
          '#default_value' => empty($field_value) ? $cell_default : $field_value,
          '#prefix' => '<td style="width:' . floor(100 / $count_cols) . '%">',
          '#suffix' => '</td>',
        );
      }
    }
    $element['tablefield']['c_break' . $i] = array(
      '#markup' => '</tr>',
    );
  }
  $element['tablefield']['t_break' . $i] = array(
    '#markup' => '</table>',
  );

  // Allow the user to add more rows/columns
  $element['tablefield']['rebuild'] = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#title' => t('Change number of rows/columns.'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $element['tablefield']['rebuild']['count_cols'] = array(
    '#title' => t('How many Columns'),
    '#type' => 'textfield',
    '#size' => 5,
    '#prefix' => '<div class="clearfix">',
    '#suffix' => '</div>',
    '#value' => $count_cols,
  );
  $element['tablefield']['rebuild']['count_rows'] = array(
    '#title' => t('How many Rows'),
    '#type' => 'textfield',
    '#size' => 5,
    '#prefix' => '<div class="clearfix">',
    '#suffix' => '</div>',
    '#value' => $count_rows,
  );
  $element['tablefield']['rebuild']['rebuild'] = array(
    '#type' => 'button',
    '#validate' => array(),
    '#limit_validation_errors' => array(),
    '#executes_submit_callback' => TRUE,
    '#submit' => array(
      'tablefield_rebuild_form',
    ),
    '#value' => t('Rebuild Table'),
    '#name' => 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta,
    '#attributes' => array(
      'class' => array(
        'tablefield-rebuild',
      ),
    ),
    '#ajax' => array(
      'callback' => 'tablefield_rebuild_form_ajax',
      'wrapper' => 'tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper',
      'effect' => 'fade',
      'event' => 'click',
    ),
  );

  // Allow the user to import a csv file
  $element['tablefield']['import'] = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#title' => t('Import from CSV'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $element['tablefield']['import']['tablefield_csv_' . $field['field_name'] . '_' . $delta] = array(
    '#title' => 'File upload',
    '#type' => 'file',
  );
  $element['tablefield']['import']['rebuild_' . $field['field_name'] . '_' . $delta] = array(
    '#type' => 'button',
    '#validate' => array(),
    '#limit_validation_errors' => array(),
    '#executes_submit_callback' => TRUE,
    '#submit' => array(
      'tablefield_rebuild_form',
    ),
    '#value' => t('Upload CSV'),
    '#name' => 'tablefield-import-button-' . $field['field_name'] . '-' . $delta,
    '#attributes' => array(
      'class' => array(
        'tablefield-rebuild',
      ),
    ),
    '#ajax' => array(
      'callback' => 'tablefield_rebuild_form_ajax',
      'wrapper' => 'tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper',
      'effect' => 'fade',
      'event' => 'click',
    ),
  );

  // Allow the user to select input filters
  if (!empty($field['settings']['cell_processing'])) {
    $element['#base_type'] = $element['#type'];
    $element['#type'] = 'text_format';
    $element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL;
  }
  return $element;
}

/**
 * Helper function to import data from a CSV file
 * @param array $form
 * @param array $form_state
 */
function tablefield_import_csv($form, &$form_state) {

  // Look for the field name by checking on the clicked button
  if (preg_match('/tablefield-import-button-(.*)$/', $form_state['clicked_button']['#name'], $id)) {

    // Extract the field and file name from the id of the clicked button
    $file_name = preg_replace('/\\-/', '_', $id[1]);
    preg_match('/_([0-9]+)$/', $file_name, $field_delta);

    // Extract the field delta from the field name
    $delta = $field_delta[1];
    $field_name = preg_replace('/' . $field_delta[0] . '/', '', $file_name);
    $language = isset($form['language']['#value']) ? $form['language']['#value'] : 'und';
    $file = file_save_upload($field_name, array(
      'file_validate_extensions' => array(
        'csv',
      ),
    ));
    if (is_object($file)) {
      if (($handle = fopen($file->uri, "r")) !== FALSE) {
        tablefield_delete_table_values($form_state['values'][$field_name][$language][$delta]['tablefield']);

        // Populate CSV values
        $max_col_count = 0;
        $row_count = 0;
        while (($csv = fgetcsv($handle, 0, ",")) !== FALSE) {
          $col_count = count($csv);
          foreach ($csv as $col_id => $col) {
            $form_state['input'][$field_name][$language][$delta]['tablefield']['cell_' . $row_count . '_' . $col_id] = $form_state['values'][$field_name][$language][$delta]['tablefield']['cell_' . $row_count . '_' . $col_id] = $col;
          }
          $max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
          $row_count++;
        }
        fclose($handle);
        $form_state['input'][$field_name][$language][$delta]['tablefield']['rebuild']['count_cols'] = $form_state['values'][$field_name][$language][$delta]['tablefield']['rebuild']['count_cols'] = $max_col_count;
        $form_state['input'][$field_name][$language][$delta]['tablefield']['rebuild']['count_rows'] = $form_state['values'][$field_name][$language][$delta]['tablefield']['rebuild']['count_rows'] = $row_count;
        drupal_set_message(t('Successfully imported @file', array(
          '@file' => $file->filename,
        )));
      }
      else {
        drupal_set_message(t('There was a problem importing @file.', array(
          '@file' => $file->filename,
        )));
      }
    }
  }
}

/**
 * Helper function to remove all values in a particular table
 * @param array $tablefield
 */
function tablefield_delete_table_values(&$tablefield) {

  // Empty out previously entered values
  foreach ($tablefield as $key => $value) {
    if (strpos($key, 'cell_') === 0) {
      $tablefield[$key] = '';
    }
  }
}

/**
 * AJAX callback to rebuild the number of rows/columns.
 * The basic idea is to descend down the list of #parent elements of the
 * clicked_button in order to locate the tablefield inside of the $form
 * array. That is the element that we need to return.
 * @param array $form
 * @param array $form_state
 */
function tablefield_rebuild_form_ajax($form, $form_state) {
  $rebuild = $form;
  $parents = $form_state['clicked_button']['#parents'];
  if ($form['#id'] == 'field-ui-field-edit-form') {
    $rebuild = $form['instance']['default_value_widget'][$parents[0]];
  }
  else {
    foreach ($parents as $parent) {

      // Locate the delta of the field - 0 deltas need to break after
      // descending to the 'rebuild' element, but deltas greater than
      // 0 need to break /before/ adding the 'rebuild' element.
      if (is_int($parent)) {
        $delta = $parent;
      }
      $tmp = $rebuild;
      if ($parent == 'rebuild' || $parent == 'import') {
        $rebuild = $delta == 0 ? $tmp[$parent] : $tmp;

        //$rebuild = $tmp[$parent];
        break;
      }
      $rebuild = $tmp[$parent];
    }
  }

  // We don't want to re-send the format/_weight options.
  unset($rebuild['format']);
  unset($rebuild['_weight']);

  // We need to avoid sending headers or the multipart form
  // will make it fail. So, we need to explicitly define the
  // whole response to ajax_deliver().
  return array(
    '#type' => 'ajax',
    '#header' => FALSE,
    '#commands' => array(
      ajax_command_insert(NULL, drupal_render($rebuild)),
      ajax_command_prepend(NULL, theme('status_messages')),
    ),
  );
}

// @todo how do we persist form_state.values for non-node entities????
//  - e.g. what is the generic way to do something like node_form_submit_build_node?
//  - it appears to work as expected in other entity forms, but keep testing nonetheless

/**
 * Helper function to rebuild the table structure w/o submitting the form.
 * @param array $form
 * @param array $form_state
 */
function tablefield_rebuild_form($form, &$form_state) {

  // Maintain the tablefield data.
  $form_state['tablefield_rebuild'] = $form_state['input'];
  $form_state['rebuild'] = true;

  // Maintain the submitted node values. This is similar
  // to how the node preview works.
  if (isset($form['#node_edit_form'])) {
    $node = node_form_submit_build_node($form, $form_state);
  }
}

/**
 * Helper function to turn form elements into a structured array.
 *
 * @param array $tablefield
 *  The table as it appears in FAPI.
 */
function tablefield_rationalize_table($tablefield) {
  $tabledata = array();

  // Remove exterraneous form data
  $count_cols = $tablefield['rebuild']['count_cols'];
  $count_rows = $tablefield['rebuild']['count_rows'];
  unset($tablefield['rebuild']);
  unset($tablefield['import']);

  // Rationalize the table data
  if (!empty($tablefield)) {
    foreach ($tablefield as $key => $value) {
      preg_match('/cell_(.*)_(.*)/', $key, $cell);

      // $cell[1] is row count $cell[2] is col count
      if ((int) $cell[1] < $count_rows && $cell['2'] < $count_cols) {
        $tabledata[$cell[1]][$cell[2]] = $value;
      }
    }
  }
  return $tabledata;
}

/**
 * Implements hook_theme().
 */
function tablefield_theme() {
  return array(
    'tablefield_view' => array(
      'variables' => array(
        'header' => NULL,
        'rows' => NULL,
        'delta' => NULL,
      ),
    ),
  );
}

/**
 * Theme function for table view
 */
function theme_tablefield_view($variables) {
  $attributes = array(
    'id' => 'tablefield-' . $variables['delta'],
    'class' => array(
      'tablefield',
    ),
  );
  return '<div id="tablefield-wrapper-' . $variables['delta'] . '" class="tablefield-wrapper">' . theme('table', array(
    'header' => $variables['header'],
    'rows' => $variables['rows'],
    'attributes' => $attributes,
  )) . '</div>';
}

Functions

Namesort descending Description
tablefield_delete_table_values Helper function to remove all values in a particular table
tablefield_field_formatter_info Implementation of hook_field_formatter_info().
tablefield_field_formatter_view Implements hook_field_formatter_view().
tablefield_field_info Implements hook_field_info().
tablefield_field_is_empty Implementation of hook_field_is_empty().
tablefield_field_load Implements hook_field_load().
tablefield_field_presave Implements hook_field_presave().
tablefield_field_schema Implements hook_field_schema().
tablefield_field_settings_form Implements hook_field_settings_form().
tablefield_field_validate Implements hook_field_validate().
tablefield_field_widget_error Implements hook_field_widget_error().
tablefield_field_widget_form Implements hook_widget_form().
tablefield_field_widget_info Implements hook_field_widget_info().
tablefield_import_csv Helper function to import data from a CSV file
tablefield_rationalize_table Helper function to turn form elements into a structured array.
tablefield_rebuild_form Helper function to rebuild the table structure w/o submitting the form.
tablefield_rebuild_form_ajax AJAX callback to rebuild the number of rows/columns. The basic idea is to descend down the list of #parent elements of the clicked_button in order to locate the tablefield inside of the $form array. That is the element that we need to return.
tablefield_theme Implements hook_theme().
theme_tablefield_view Theme function for table view