You are here

tablefield.module in TableField 7.2

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

Provides a set of fields that can be used to store tabular data with a node.

@todo Should we create a helper function for sanitization?

  • We should see if it makes sense to sanitize on load as well as view.

File

tablefield.module
View source
<?php

/**
 * @file
 * Provides a set of fields that can be used to store tabular data with a node.
 *
 * @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_help().
 */
function tablefield_help($path, $arg) {
  switch ($path) {
    case 'admin/help#tablefield':

      // Return a line-break version of the README.txt.
      return _filter_autop(file_get_contents(dirname(__FILE__) . '/README.txt'));
  }
}

/**
 * Implements hook_menu().
 */
function tablefield_menu() {
  return array(
    'tablefield/export/%/%/%/%/%' => array(
      'page callback' => 'tablefield_export_csv',
      'page arguments' => array(
        2,
        3,
        4,
        5,
        6,
      ),
      'title' => 'Export Table Data',
      'access arguments' => array(
        'export tablefield',
      ),
    ),
    'admin/config/content/tablefield' => array(
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'tablefield_admin_settings_form',
      ),
      'title' => 'Tablefield',
      'description' => 'Global configuration for the Tablefield module.',
      'access arguments' => array(
        'configure tablefield',
      ),
    ),
  );
}

/**
 * Menu callback to prepare administration configuration form.
 */
function tablefield_admin_settings_form() {
  $form = array();
  $form['tablefield_csv_separator'] = array(
    '#type' => 'textfield',
    '#title' => t('CSV separator'),
    '#size' => 1,
    '#maxlength' => 1,
    '#default_value' => variable_get('tablefield_csv_separator', ','),
    '#description' => t('Select the separator for the CSV import/export.'),
  );
  return system_settings_form($form);
}

/**
 * Implements hook_permission().
 */
function tablefield_permission() {
  return array(
    'export tablefield' => array(
      'title' => t('Export Tablefield Data as CSV'),
    ),
    'rebuild tablefield' => array(
      'title' => t('Rebuild any tablefield'),
    ),
    'configure tablefield' => array(
      'title' => t('Allow changes in the global tablefield module configuration'),
    ),
  );
}

/**
 * Menu callback to export a table as a CSV.
 *
 * @param string $entity_type
 *   The type of entity, e.g. node.
 * @param string $entity_id
 *   The id of the entity.
 * @param string $field_name
 *   The machine name of the field to load.
 * @param string $langcode
 *   The language code specified.
 * @param string $delta
 *   The field delta to load.
 */
function tablefield_export_csv($entity_type, $entity_id, $field_name, $langcode, $delta) {
  $filename = sprintf('%s_%s_%s_%s_%s.csv', $entity_type, $entity_id, $field_name, $langcode, $delta);
  $uri = 'temporary://' . $filename;

  // Load the entity.
  $entities = entity_load($entity_type, array(
    $entity_id,
  ));
  if (empty($entities)) {
    return MENU_NOT_FOUND;
  }
  $entity = reset($entities);
  if (!_tablefield_entity_access('view', $entity_type, $entity) || !field_access('view', $field_info, $entity_type, $entity)) {
    return MENU_ACCESS_DENIED;
  }

  // Ensure that the data is available and that we can load a
  // temporary file to stream the data.
  if (isset($entity->{$field_name}[$langcode][$delta]['value']) && ($fp = fopen($uri, 'w+'))) {
    $table = unserialize($entity->{$field_name}[$langcode][$delta]['value']);

    // Save the data as a CSV file.
    foreach ($table['tabledata'] as $row) {

      // Remove the weight column.
      array_pop($row);
      fputcsv($fp, $row, variable_get('tablefield_csv_separator', ','));
    }
    fclose($fp);

    // Add basic HTTP headers.
    $http_headers = array(
      'Content-Type' => 'text/csv',
      'Content-Disposition' => 'attachment; filename="' . $filename . '"',
      'Content-Length' => filesize($uri),
    );

    // IE needs special headers.
    if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
      $http_headers['Cache-Control'] = 'must-revalidate, post-check=0, pre-check=0';
      $http_headers['Pragma'] = 'public';
    }
    else {
      $http_headers['Pragma'] = 'no-cache';
    }

    // Stream the download.
    file_transfer($uri, $http_headers);
  }

  // Something went wrong.
  drupal_add_http_header('Status', '500 Internal Server Error');
  print t('Error generating CSV.');
  drupal_exit();
}

/**
 * Helper function: A sort-of copy of entity_access() from entity API.
 *
 * @todo Remove if entity_access() ends up in core.
 */
function _tablefield_entity_access($op, $entity_type, $entity = NULL, $account = NULL) {
  $info = entity_get_info($entity_type);

  // Use entity API access callbacks if provided.
  if (isset($info[$entity_type]['access callback'])) {
    $access_callback = $info[$entity_type]['access callback'];
    return $access_callback($op, $entity, $account, $entity_type);
  }
  elseif ($entity_type == 'node') {
    return node_access($op, $entity, $account);
  }
  elseif ($entity_type == 'user') {
    return user_view_access($entity);
  }
  elseif ($entity_type == 'taxonomy_term') {
    if (user_access('administer taxonomy', $account)) {
      return TRUE;
    }
    if (user_access('access content', $account)) {
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * 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' => 'tablefield_default',
      'property_type' => 'tablefield',
      'property_callbacks' => array(
        'tablefield_property_info_callback',
      ),
    ),
  );
}

/**
 * Defines info for the properties of the tablefield field data structure.
 */
function tablefield_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $property =& $info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';

  // Needed for Search API indexing.
  $property['property info'] = tablefield_item_property_info();
  unset($property['query callback']);
}

/**
 * Define metadata about item properties. Search API indexing addition.
 */
function tablefield_item_property_info() {
  $properties['table_value'] = array(
    'type' => 'text',
    'label' => t('The value of the table.'),
    'computed' => TRUE,
    'getter callback' => 'tablefield_get_table_value',
  );
  return $properties;
}

/**
 * Get the property just as it is set in the data. Search API indexing addition.
 */
function tablefield_get_table_value($data, array $options, $name, $type, $info) {
  if (isset($data['tabledata'])) {
    $data['value'] = '';
    foreach ($data['tabledata'] as $rows) {
      $data['value'] .= implode(" ", $rows) . " ";
    }
  }
  return trim($data['value']);
}

/**
 * Implements hook_field_settings_form().
 */
function tablefield_field_settings_form($field, $instance, $has_data) {
  $form = array();
  $form['restrict_rebuild'] = array(
    '#type' => 'checkbox',
    '#title' => t('Restrict rebuilding to users with the permission "rebuild tablefield"'),
    '#default_value' => isset($field['settings']['restrict_rebuild']) ? $field['settings']['restrict_rebuild'] : FALSE,
  );
  $form['lock_values'] = array(
    '#type' => 'checkbox',
    '#title' => t('Lock table header so default values cannot be changed.'),
    '#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_prepare_view().
 */
function tablefield_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {

  // Reset the array's internal pointer to the first element.
  $instance = reset($instances);
  foreach ($entities as $id => $entity) {
    $entity_items =& $items[$id];
    tablefield_field_presave($entity_type, $entity, $field, $instance, $langcode, $entity_items);
  }
}

/**
 * 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();
      if (!empty($table['tablefield'])) {
        unset($tablefield['tablefield']);

        // Sort by weight.
        uasort($table['tablefield']['tabledata'], 'drupal_sort_weight');

        // Put the data in the desired order before saving.
        $row_counter = $col_counter = 0;
        foreach ($table['tablefield']['tabledata'] as $row) {
          foreach ($row as $key => $cell) {
            if ($key === 'weight') {
              $tablefield['tablefield']['tabledata']['row_' . $row_counter]['weight'] = $cell;
            }
            else {
              $tablefield['tablefield']['tabledata']['row_' . $row_counter]['col_' . $col_counter] = $cell;
            }
            $col_counter++;
          }
          $row_counter++;
          $col_counter = 0;
        }

        // Clear the old table data and repopulate it with the new values.
        unset($table['tablefield']['tabledata']);
        $table['tablefield']['tabledata'] = $tablefield['tablefield']['tabledata'];

        // Add the non-value data back in before we save.
        $tablefield = array_merge($tablefield, $table);
      }
      $items[$delta]['value'] = isset($tablefield['tablefield']) ? serialize($tablefield['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) {

  // 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 ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => $item) {
      if (isset($item['value'])) {
        $items[$id][$delta]['tabledata'] = unserialize($item['value']);
      }
    }
  }
}

/**
 * Implements 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;
  }

  // Check for already serialized data. This is the case for other language
  // versions of this field.
  if (!empty($item['tabledata'])) {
    return FALSE;
  }
  $count_rows = $item['tablefield']['rebuild']['count_rows'];

  // Remove the preference fields to see if the table cells are all empty.
  unset($item['tablefield']['caption']);
  unset($item['tablefield']['rebuild']);
  unset($item['tablefield']['import']);
  unset($item['tablefield']['paste']);
  if (!empty($item['tablefield']['tabledata'])) {
    for ($i = 0; $i < $count_rows; $i++) {
      foreach ($item['tablefield']['tabledata']["row_{$i}"] as $key => $cell) {

        // Keys denoting weight data and anything still iterateable should be
        // ignored while checking for empty cell data.
        if (strpos($key, 'weight') === FALSE && !is_array($cell) && !empty($cell)) {
          return FALSE;
        }
      }
    }
  }
  return TRUE;
}

/**
 * Implements hook_field_formatter_info().
 */
function tablefield_field_formatter_info() {
  return array(
    'tablefield_default' => array(
      'label' => t('Tabular view'),
      'field types' => array(
        'tablefield',
      ),
      'settings' => array(
        'sticky_header' => TRUE,
        'hide_header' => FALSE,
        'hide_empty_rows' => FALSE,
        'hide_empty_cols' => FALSE,
        'hide_cols_skip_head' => FALSE,
        'trim_trailing_cols' => FALSE,
        'trim_trailing_rows' => FALSE,
        'export_csv' => FALSE,
      ),
    ),
    'format_raw' => array(
      'label' => t('Raw data (JSON)'),
      'field types' => array(
        'tablefield',
      ),
      'settings' => array(
        'usearraykeys' => 'No',
        'vertheader' => FALSE,
        'tabledataonly' => FALSE,
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function tablefield_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $summary = array();
  switch ($display['type']) {
    case 'format_raw':
      $summary[] = t('Use first row/column values as array keys (if not empty): %tr', array(
        '%tr' => $settings['usearraykeys'],
      ));
      $summary[] = t('Vertical header (first column instead of first row): %tr', array(
        '%tr' => $settings['vertheader'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Table data only (no caption): %tr', array(
        '%tr' => $settings['tabledataonly'] ? t('Yes') : t('No'),
      ));
      break;
    default:
      $summary[] = t('Sticky header: %tr', array(
        '%tr' => $settings['sticky_header'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Hide table header: %tr', array(
        '%tr' => $settings['hide_header'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Hide empty columns ignoring column header: %tr', array(
        '%tr' => $settings['hide_cols_skip_head'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Trim empty trailing rows: %tr', array(
        '%tr' => $settings['trim_trailing_rows'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Trim empty trailing columns: %tr', array(
        '%tr' => $settings['trim_trailing_cols'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Hide empty rows: %tr', array(
        '%tr' => $settings['hide_empty_rows'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Hide empty columns: %tr', array(
        '%tr' => $settings['hide_empty_cols'] ? t('Yes') : t('No'),
      ));
      $permission = l(t('permission'), 'admin/people/permissions', array(
        'fragment' => 'module-tablefield',
        'attributes' => array(
          'title' => t('Manage user permissions'),
        ),
      ));
      $summary[] = t('Show link to export table data as CSV depending on !permission: %tr', array(
        '%tr' => $settings['hide_cols_skip_head'] ? t('Yes') : t('No'),
        '!permission' => $permission,
      ));
  }
  return implode('<br />', $summary);
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function tablefield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();
  switch ($display['type']) {
    case 'format_raw':
      $element['usearraykeys'] = array(
        '#type' => 'select',
        '#title' => t('Use first row/column values as array keys (if not empty)'),
        '#options' => array(
          'No' => t('No'),
          'Header' => t('Header only'),
          'Both' => t('Both first row and first column (two headers, horizontal and vertical)'),
        ),
        '#default_value' => $settings['usearraykeys'],
      );
      $element['vertheader'] = array(
        '#title' => t('Vertical header (first column instead of first row)'),
        '#type' => 'checkbox',
        '#default_value' => $settings['vertheader'],
      );
      $element['tabledataonly'] = array(
        '#title' => t('Table data only (no caption)'),
        '#type' => 'checkbox',
        '#default_value' => $settings['tabledataonly'],
      );
      break;
    default:
      $element['sticky_header'] = array(
        '#title' => t('Sticky header'),
        '#type' => 'checkbox',
        '#default_value' => $settings['sticky_header'],
      );
      $element['hide_header'] = array(
        '#title' => t('Hide table header row'),
        '#type' => 'checkbox',
        '#default_value' => $settings['hide_header'],
      );
      $element['hide_cols_skip_head'] = array(
        '#title' => t('Hide empty columns ignoring column header'),
        '#type' => 'checkbox',
        '#default_value' => $settings['hide_cols_skip_head'],
      );
      $element['trim_trailing_cols'] = array(
        '#title' => t('Trim empty trailing columns'),
        '#type' => 'checkbox',
        '#default_value' => $settings['trim_trailing_cols'],
      );
      $element['trim_trailing_rows'] = array(
        '#title' => t('Trim empty trailing rows'),
        '#type' => 'checkbox',
        '#default_value' => $settings['trim_trailing_rows'],
      );
      $element['hide_empty_rows'] = array(
        '#title' => t('Hide empty rows'),
        '#type' => 'checkbox',
        '#default_value' => $settings['hide_empty_rows'],
      );
      $element['hide_empty_cols'] = array(
        '#title' => t('Hide empty columns'),
        '#type' => 'checkbox',
        '#default_value' => $settings['hide_empty_cols'],
      );
      $element['hide_cols_skip_head'] = array(
        '#title' => t('Hide empty columns ignoring column header'),
        '#type' => 'checkbox',
        '#default_value' => $settings['hide_cols_skip_head'],
      );
      $permission = l(t('permission'), 'admin/people/permissions', array(
        'fragment' => 'module-tablefield',
        'attributes' => array(
          'title' => t('Manage user permissions'),
        ),
      ));
      $element['export_csv'] = array(
        '#title' => t('Show link to export table data as CSV depending on !permission', array(
          '!permission' => $permission,
        )),
        '#type' => 'checkbox',
        '#default_value' => $settings['export_csv'],
      );
  }
  return $element;
}

/**
 * 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'];
  $prettyprint = FALSE;
  if (!empty($items)) {
    $i = 0;
    $len = count($items);
    foreach ($items as $delta => $table) {

      // If we are not on the last iteration of the loop use a separator.
      $separator = $i == $len - 1 ? '' : ',';
      switch ($display['type']) {
        case 'format_raw':
          $value = unserialize($table['value']);

          // Swap rows and columns if the header is vertical (first column).
          if ($settings['vertheader']) {
            $transposed = tablefield_transpose($value['tabledata']);
            unset($value['tabledata']);
            array_pop($transposed);

            // Swap column and row key names.
            foreach ($transposed as $colk => $colv) {
              foreach ($colv as $rowk => $rowv) {
                $value['tabledata'][str_replace('col_', 'row_', $colk)][str_replace('row_', 'col_', $rowk)] = $rowv;
              }

              // Add a 'weight' although the value is not relevant.
              $value['tabledata'][str_replace('col_', 'row_', $colk)]['weight'] = 0;
            }
          }

          // Remove unneeded data.
          unset($value['rebuild']);
          unset($value['import']);
          unset($value['paste']);
          if ($settings['usearraykeys'] === 'Header' || $settings['usearraykeys'] === 'Both') {

            // Keep first row values (header) to use for array keys later.
            $keys = $value['tabledata']['row_0'];

            // If a header value is empty use the column key (col_#).
            foreach ($keys as $key => $content) {
              if (empty($content)) {
                $keys[$key] = $key;
              }
            }

            // Warning about columns with duplicate names being suppressed.
            $unique = array_unique($keys);
            $duplicates = array_diff_assoc($keys, $unique);
            foreach ($duplicates as $duplicate) {
              drupal_set_message(t('The column header "%key" appears multiple times in a table. In the JSON output only the last column with this key is used to avoid duplicate names.', array(
                '%key' => $duplicate,
              )), 'warning', FALSE);
            }
            unset($keys['weight']);

            // Remove the first row (header).
            unset($value['tabledata']['row_0']);
            if ($settings['usearraykeys'] === 'Both') {

              // Remove the first column from the keys.
              unset($keys['col_0']);

              // Make number of elements same as $row for array_combine later.
              unset($keys['weight']);
            }
          }
          foreach ($value['tabledata'] as $key => $row) {
            unset($row['weight']);
            if ($settings['usearraykeys'] === 'Header' || $settings['usearraykeys'] === 'Both') {
              if ($settings['usearraykeys'] === 'Both') {
                $row_ident = $row['col_0'];

                // Clean up unneeded data.
                unset($value['tabledata'][$key]);
                unset($row['col_0']);
                $row = array_combine($keys, $row);

                // If the first column value is empty use the row key (row_#).
                $row_id = empty($row_ident) ? $key : $row_ident;

                // Warning about rows with duplicate names being suppressed.
                if (isset($value['tabledata'][$row_id])) {
                  drupal_set_message(t('The row header "%row_id" appears multiple times in a table. In the JSON output only the last row with this key is used to avoid duplicate names.', array(
                    '%row_id' => $row_id,
                  )), 'warning', FALSE);
                }
                $value['tabledata'][$row_id] = $row;

                // Remove the old key from the data set if it was replaced.
                if (!empty($row_ident)) {
                  unset($value['tabledata'][$key]);
                }
              }
              $unique = array_unique($keys);
              $row = array_combine($unique, $row);
            }
            if ($settings['usearraykeys'] != 'Both') {
              $value['tabledata'][$key] = $row;
            }
          }

          // DEVELOPERS! Extra future data should be unset below (e.g. title).
          if ($settings['tabledataonly']) {
            unset($value['caption']);
            $value = reset($value);
          }

          // Assign the markup showing pretty print on node pages.
          if (menu_get_object()) {
            $prettyprint = TRUE;
            $element[$delta] = array(
              '#markup' => str_replace(array(
                "\r",
                "\n",
                "\t",
              ), "<br />", check_plain(json_encode($value, JSON_PRETTY_PRINT))) . $separator,
            );
          }
          else {
            $element[$delta] = array(
              '#markup' => check_plain(drupal_json_encode($value)) . $separator,
            );
          }
          break;
        default:

          // Check for table caption.
          $raw = unserialize($table['value']);
          $caption = isset($raw['caption']) ? check_plain($raw['caption']) : '';

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

          // Run the table through input filters.
          if (isset($tabledata['tabledata'])) {
            if (!empty($tabledata['tabledata'])) {
              if ($settings['trim_trailing_rows']) {
                $tabledata['tabledata'] = tablefield_trim($tabledata['tabledata']);
              }
              if ($settings['trim_trailing_cols']) {
                $tabledata['tabledata'] = tablefield_rtrim_cols($tabledata['tabledata']);
              }
              if ($settings['hide_empty_rows']) {
                $tabledata['tabledata'] = tablefield_hide_rows($tabledata['tabledata']);
              }
              if ($settings['hide_empty_cols']) {
                $tabledata['tabledata'] = tablefield_hide_cols($tabledata['tabledata']);
              }
              if ($settings['hide_cols_skip_head']) {
                $tabledata['tabledata'] = tablefield_hide_cols($tabledata['tabledata'], TRUE);
              }
              foreach ($tabledata['tabledata'] as $row_key => $row) {
                foreach ($row as $col_key => $cell) {
                  if (!empty($table['format']) && $col_key !== 'weight') {
                    $tabledata[$row_key][$col_key] = array(
                      'data' => check_markup($cell, $table['format']),
                      'class' => array(
                        $row_key,
                        $col_key,
                      ),
                    );
                  }
                  elseif ($col_key !== 'weight') {
                    $tabledata[$row_key][$col_key] = array(
                      'data' => check_plain($cell),
                      'class' => array(
                        $row_key,
                        $col_key,
                      ),
                    );
                  }
                }
              }
            }

            // Pull the header for theming.
            unset($tabledata['caption']);
            unset($tabledata['tabledata']);
            unset($tabledata['rebuild']);
            unset($tabledata['import']);
            unset($tabledata['paste']);
            $header_data = isset($tabledata['row_0']) ? $tabledata['row_0'] : NULL;

            // Check for an empty header, if so we don't want to theme it.
            $noheader = TRUE;
            if (empty($settings['hide_header']) && $header_data) {
              foreach ($header_data as $cell) {
                if (strlen($cell['data']) > 0) {
                  $noheader = FALSE;
                  break;
                }
              }
            }
            $header = $noheader ? NULL : $header_data;
            $entity_info = entity_get_info($entity_type);
            $entity_id = !empty($entity_info['entity keys']['id']) ? $entity->{$entity_info['entity keys']['id']} : NULL;

            // Remove the first row from the tabledata.
            array_shift($tabledata);

            // Theme the table for display.
            $element[$delta] = array(
              '#theme' => 'tablefield_view',
              '#attributes' => array(
                'id' => drupal_html_id('tablefield-' . $entity_type . '-' . $entity_id . '-' . $field['field_name'] . '-' . $delta),
                'class' => array(
                  'tablefield',
                ),
              ),
              '#caption' => $caption,
              '#sticky' => isset($settings['sticky_header']) ? $settings['sticky_header'] : NULL,
              '#header' => $header,
              '#rows' => $tabledata,
              '#delta' => $delta,
              '#export' => isset($settings['export_csv']) ? $settings['export_csv'] : NULL,
              '#entity_type' => $entity_type,
              '#entity_id' => $entity_id,
              '#field_name' => $field['field_name'],
              '#langcode' => $langcode,
              '#formatter' => $formatter,
            );
          }
      }
      $i++;
    }
  }
  if ($prettyprint) {
    $element['#prefix'] = '<pre>';
    $element['#suffix'] = '</pre>';
  }
  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) {
  $form['#after_build'][] = 'tablefield_after_build';
  $settings = isset($instance['display']['default']['settings']) ? $instance['display']['default']['settings'] : FALSE;
  $element['#type'] = 'tablefield';
  $form['#attributes']['enctype'] = 'multipart/form-data';

  /* Tablefield is sometimes embedded within another form by other modules, such
   * as Field Collection. Because of that, we cannot rely on field_name and
   * $delta to provide a unique ID for this element. Instead we use a
   * concatenation of the field parents along with the current field name,
   * language, delta and tablefield key.
   */
  $tablefield_parents = isset($element['#field_parents']) ? $element['#field_parents'] : array();
  array_push($tablefield_parents, $element['#field_name'], $element['#language'], $element['#delta'], 'tablefield');
  $id = drupal_clean_css_identifier(implode('-', $tablefield_parents));

  // IDs to use for various buttons/wrapper for this element. When processing
  // an AJAX request, these IDs are used to build the field stack so we know
  // where the value we're adjusting is in the FormAPI array.
  $ajax_wrapper_id = "{$id}-wrapper";
  $rebuild_id = "{$id}-rebuild";
  $import_id = "{$id}-import";
  $pasted_id = "{$id}-pasted";

  // Default table size.
  $default_size['count_cols'] = isset($instance['default_value'][0]['tablefield']['rebuild']['count_cols']) ? $instance['default_value'][0]['tablefield']['rebuild']['count_cols'] : 5;
  $default_size['count_rows'] = isset($instance['default_value'][0]['tablefield']['rebuild']['count_rows']) ? $instance['default_value'][0]['tablefield']['rebuild']['count_rows'] : 5;

  // Default table cell values.
  $default_value = NULL;

  // If there's a triggering element get the values from form state.
  if (isset($form_state['triggering_element'])) {
    if ($form_state['triggering_element']['#name'] == $rebuild_id) {

      // Rebuilding table rows/cols.
      $default_value = drupal_array_get_nested_value($form_state['tablefield_rebuild'], $tablefield_parents);
      drupal_set_message(t('Table structure rebuilt.'), 'status', FALSE);
    }
    elseif ($form_state['triggering_element']['#name'] == $import_id) {

      // Importing CSV data.
      tablefield_import_csv($form, $form_state, $langcode, $import_id, $tablefield_parents);
      $default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
    }
    elseif ($form_state['triggering_element']['#name'] == $pasted_id) {

      // Importing pasted data.
      tablefield_import_pasted($form, $form_state, $langcode, $pasted_id, $tablefield_parents);
      $default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
      if (empty($default_value['rebuild'])) {
        $default_value['rebuild'] = $default_size;
      }
    }
    else {

      // The triggering element is neither a rebuild nor an import
      // e.g. a file upload.
      $default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
    }
  }

  // If no values by now, get tablefield item value stored in database, if any.
  if (!$default_value) {

    // This could be set e.g. when using paragraphs module.
    if (isset($items[$delta]['tablefield'])) {
      $default_value = $items[$delta]['tablefield'];
    }
    elseif (isset($items[$delta]['value'])) {
      $default_value = unserialize($items[$delta]['value']);
    }
    elseif ($delta === 0 && isset($instance['default_value'][0]['tablefield'])) {
      $default_value = $instance['default_value'][0]['tablefield'];
    }
  }
  if (empty($default_value['rebuild'])) {
    $default_value['rebuild'] = $default_size;
  }
  else {
    $default_size = $default_value['rebuild'];
  }
  $count_rows = $default_size['count_rows'];
  $count_cols = $default_size['count_cols'];

  // Now we can build the widget.
  if (!empty($instance['description'])) {
    $help_text = $instance['description'];
  }
  else {
    if ($settings) {
      $display_settings = l(t('default display settings'), 'admin/structure/types/manage/' . $element['#bundle'] . '/display', array(
        'attributes' => array(
          'title' => t('Manage display settings'),
        ),
      ));
      if ($settings['hide_header'] == TRUE) {
        $help_text = t('This table will not have a header according to the !display_settings.', array(
          '!display_settings' => $display_settings,
        ));
      }
      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' => filter_xss_admin($help_text),
    '#attributes' => array(
      'id' => $id,
      'class' => array(
        'form-tablefield',
      ),
    ),
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#collapsible' => FALSE,
    '#prefix' => '<div id="' . $ajax_wrapper_id . '">',
    '#suffix' => '</div>',
  );
  if ($settings) {
    if ($settings['hide_header']) {
      $element['tablefield']['#attributes']['class'][] = 'table-no-headers';
    }
  }

  // Give the fieldset the appropriate class if it is required.
  if ($element['#required']) {
    $element['tablefield']['#title'] .= ' <span class="form-required" title="';
    $element['tablefield']['#title'] .= t('This field is required');
    $element['tablefield']['#title'] .= '">*</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.');
    if ($settings) {
      if (!$settings['hide_header']) {
        $element['tablefield']['#description'] .= ' ' . t('The first row will appear as the table header. Leave the first row blank if you do not need a header.');
      }
    }
  }

  // Render the form table.
  $element['tablefield']['tabledata']['a_break'] = array(
    '#markup' => '<table id="tablefield-editor">',
  );
  $default_value = isset($default_value['tabledata']) ? $default_value['tabledata'] : $default_value;

  // Loop over all the rows.
  for ($i = 0; $i < $count_rows; $i++) {
    $zebra = $i % 2 == 0 ? 'even' : 'odd';
    $element['tablefield']['tabledata']['b_break' . $i] = array(
      '#markup' => '<tr class="draggable tablefield-row-' . $i . ' ' . $zebra . '"><td class="tablefield-row-count">' . ($i + 1) . '</td>',
    );

    // Loop over all the columns.
    for ($ii = 0; $ii < $count_cols; $ii++) {
      $instance_default = isset($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]["col_{$ii}"]) ? $instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]["col_{$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']['tabledata']['row_' . $i]['col_' . $ii] = array(
          '#type' => 'value',
          '#value' => $instance_default,
        );

        // Display the default value, since it's not editable.
        $element['tablefield']['tabledata']['row_' . $i]['col_' . $ii . '_display'] = array(
          '#type' => 'item',
          '#title' => $instance_default,
          '#prefix' => '<td>',
          '#suffix' => '</td>',
        );
      }
      else {
        $cell_default = isset($default_value['row_' . $i]['col_' . $ii]) ? $default_value['row_' . $i]['col_' . $ii] : '';
        $element['tablefield']['tabledata']['row_' . $i]['col_' . $ii] = array(
          '#type' => 'textfield',
          '#maxlength' => 2048,
          '#size' => 0,
          '#attributes' => array(
            'id' => $id . '-cell-' . $i . '-' . $ii,
            'class' => array(
              'tablefield-row-' . $i,
              'tablefield-col-' . $ii,
            ),
            'style' => 'min-width: 100%',
          ),
          '#default_value' => $cell_default,
          '#prefix' => '<td>',
          '#suffix' => '</td>',
        );
      }
    }

    // Add an extra column for the weight.
    $row_weight_default = isset($default_value[$i]['weight']) ? $default_value[$i]['weight'] : $i + 1;
    $element['tablefield']['tabledata']['row_' . $i]['weight'] = array(
      '#type' => 'weight',
      '#title' => t('Weight'),
      '#default_value' => $row_weight_default,
      '#delta' => $count_rows,
      '#attributes' => array(
        'class' => array(
          'tablefield-weight',
        ),
      ),
      '#prefix' => '<td class="tabledrag-hide">',
      '#suffix' => '</td>',
    );
    $element['tablefield']['c_break' . $i] = array(
      '#markup' => '</tr>',
    );
  }
  $element['tablefield']['t_break' . $i] = array(
    '#markup' => '</table>',
  );

  // Provide caption field to describe the data contained in the table.
  $element['tablefield']['caption'] = array(
    '#type' => 'textfield',
    '#title' => t('Table description'),
    '#default_value' => '',
    '#description' => t('This brief caption will be associated with the table and will help Assitive Technology, like screen readers, better describe the content within.'),
  );
  if (isset($items[$delta]['value'])) {
    $raw = unserialize($items[$delta]['value']);
    if (isset($raw['caption'])) {
      $element['tablefield']['caption']['#default_value'] = check_plain($raw['caption']);
    }
  }

  // If the user doesn't have rebuild perms, we pass along the data as a value.
  // Otherwise we provide form elements to specify the size and ajax rebuild.
  if (isset($field['settings']['restrict_rebuild']) && $field['settings']['restrict_rebuild'] && !user_access('rebuild tablefield')) {
    $element['tablefield']['rebuild'] = array(
      '#type' => 'value',
      '#tree' => TRUE,
      'count_cols' => array(
        '#type' => 'value',
        '#value' => $count_cols,
      ),
      'count_rows' => array(
        '#type' => 'value',
        '#value' => $count_rows,
      ),
      'rebuild' => array(
        '#type' => 'value',
        '#value' => t('Rebuild Table'),
      ),
    );
  }
  else {
    $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' => $rebuild_id,
      '#attributes' => array(
        'class' => array(
          'tablefield-rebuild',
        ),
      ),
      '#ajax' => array(
        'callback' => 'tablefield_rebuild_form_ajax',
        'wrapper' => $ajax_wrapper_id,
        'effect' => 'fade',
      ),
    );
  }

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

  // Allow user to paste data (e.g. from Excel).
  $element['tablefield']['paste'] = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#title' => t('Copy & Paste'),
    '#attributes' => array(
      'class' => array(
        'tablefield-extra tablefield-paste',
      ),
    ),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $delimiters = array(
    'TAB' => t('TAB'),
    ',' => t('Comma ,'),
    ';' => t('Semicolon ;'),
    '|' => t('Pipe |'),
    '+' => t('Plus +'),
    ':' => t('Colon :'),
  );
  $element['tablefield']['paste']['paste_delimiter'] = array(
    '#type' => 'select',
    '#tree' => TRUE,
    '#title' => t('Column separator'),
    '#name' => 'delimiter[' . $pasted_id . ']',
    '#options' => $delimiters,
    '#description' => t('Data copied from Excel will use TAB.'),
  );
  $element['tablefield']['paste']['data'] = array(
    '#type' => 'textarea',
    '#tree' => TRUE,
    '#name' => 'data[' . $pasted_id . ']',
    '#title' => t('Paste table data here:'),
  );
  $element['tablefield']['paste']['paste_import'] = array(
    '#type' => 'button',
    '#validate' => array(),
    '#limit_validation_errors' => array(),
    '#executes_submit_callback' => TRUE,
    '#submit' => array(
      'tablefield_rebuild_form',
    ),
    '#value' => t('Import & Rebuild'),
    '#name' => $pasted_id,
    '#ajax' => array(
      'callback' => 'tablefield_rebuild_form_ajax',
      'wrapper' => $ajax_wrapper_id,
      'effect' => 'fade',
    ),
  );

  // 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;
    if (module_exists('wysiwyg')) {
      $element['#wysiwyg'] = FALSE;
    }
  }
  return $element;
}

/**
 * Form #after_build callback for tablefield_field_widget_form().
 */
function tablefield_after_build($form, &$form_state) {
  drupal_add_css(drupal_get_path('module', 'tablefield') . '/tablefield.css');
  drupal_add_tabledrag('tablefield-editor', 'order', 'sibling', 'tablefield-weight');
  return $form;
}

/**
 * Helper function to import data from a CSV file.
 *
 * @param array $form
 *   The form structure.
 * @param array $form_state
 *   The current state of the form.
 * @param string $langcode
 *   The language associated with the form items.
 * @param string $file_form_field_name
 *   The key of the upload form element in the form array.
 * @param array $tablefield_parents
 *   The parents of the tablefield element.
 */
function tablefield_import_csv($form, &$form_state, $langcode, $file_form_field_name, $tablefield_parents) {

  // Extract the field and file name from the id of the clicked button.
  $file = file_save_upload($file_form_field_name, array(
    'file_validate_extensions' => array(
      'csv',
    ),
  ));
  if (is_object($file)) {

    // Assure Mac/Linux EOL markers work with fgetcsv().
    $auto_detect_line_endings = ini_get('auto_detect_line_endings');
    ini_set('auto_detect_line_endings', TRUE);
    if (($handle = fopen($file->uri, "r")) !== FALSE) {
      tablefield_delete_table_values(drupal_array_get_nested_value($form_state['values'], $tablefield_parents));
      tablefield_delete_table_values(drupal_array_get_nested_value($form_state['input'], $tablefield_parents));

      // Checking the encoding of the CSV file to be UTF-8.
      $encoding = 'UTF-8';
      if (function_exists('mb_detect_encoding')) {
        $file_contents = file_get_contents($file->uri);
        $encodings_list = implode(',', variable_get('tablefield_detect_encodings', array(
          'UTF-8',
          'ISO-8859-1',
          'WINDOWS-1251',
        )));
        $encoding = mb_detect_encoding($file_contents, $encodings_list);
      }

      // Populate CSV values.
      $max_col_count = 0;
      $row_count = 0;
      $imported_tablefield = array();
      while (($csv = fgetcsv($handle, 0, variable_get('tablefield_csv_separator', ','))) !== FALSE) {
        $col_count = count($csv);
        foreach ($csv as $col_id => $col) {
          $imported_tablefield['row_' . $row_count]['col_' . $col_id] = tablefield_convert_encoding($col, $encoding);
        }
        $max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
        $row_count++;
      }
      fclose($handle);
      ini_set('auto_detect_line_endings', $auto_detect_line_endings);
      $imported_tablefield['rebuild'] = array(
        'count_cols' => $max_col_count,
        'count_rows' => $row_count,
      );
      drupal_array_set_nested_value($form_state['values'], $tablefield_parents, $imported_tablefield);
      drupal_array_set_nested_value($form_state['input'], $tablefield_parents, $imported_tablefield);
      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 import pasted data.
 *
 * @param array $form
 *   The form structure.
 * @param array $form_state
 *   The current state of the form.
 * @param string $langcode
 *   The language associated with the form items.
 * @param array $tablefield_parents
 *   The parents of the tablefield element.
 */
function tablefield_import_pasted($form, &$form_state, $langcode, $pasted_form_field_name, $tablefield_parents) {
  $data = $form_state['input']['data'][$pasted_form_field_name];
  if (!empty($data)) {

    // Empty the current table.
    tablefield_delete_table_values(drupal_array_get_nested_value($form_state['values'], $tablefield_parents));
    tablefield_delete_table_values(drupal_array_get_nested_value($form_state['input'], $tablefield_parents));

    // Get the delimiter.
    $col_delimiter = $form_state['input']['delimiter'][$pasted_form_field_name];
    if ($col_delimiter == 'TAB') {
      $col_delimiter = "\t";
    }

    // Populate table with pasted values.
    $max_col_count = 0;
    $row_count = 0;
    $imported_tablefield = array();
    $rows = explode(PHP_EOL, $data);
    foreach ($rows as $row_id => $row) {

      // Explode the current row into columns:
      $cols = explode($col_delimiter, $row);
      $col_count = count($cols);
      if ($col_count > 0) {
        foreach ($cols as $col_id => $col) {
          $imported_tablefield['row_' . $row_count]['col_' . $col_id] = $col;
        }
        $max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
        $row_count++;
      }
    }

    // Rebuild the table if necessary, and save.
    $imported_tablefield['rebuild'] = array(
      'count_cols' => $max_col_count,
      'count_rows' => $row_count,
    );
    drupal_array_set_nested_value($form_state['values'], $tablefield_parents, $imported_tablefield);
    drupal_array_set_nested_value($form_state['input'], $tablefield_parents, $imported_tablefield);
    drupal_set_message(t('Successfully imported pasted data.'));
  }
  else {
    drupal_set_message(t('There was a problem importing pasted data.'));
  }
}

/**
 * Helper function to remove all values in a particular table.
 *
 * @param array $tablefield
 *   The table as it appears in FAPI.
 */
function tablefield_delete_table_values(&$tablefield) {

  // Empty out previously entered values.
  foreach ($tablefield as $key => $value) {
    if (strpos($key, 'row_') === 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.
 */
function tablefield_rebuild_form_ajax($form, $form_state) {
  $parents = $form_state['triggering_element']['#array_parents'];

  // We do not want to go as deep as rebuild/rebuild or import/import,
  // i.e. the triggering buttons.
  array_pop($parents);
  array_pop($parents);
  $rebuild = drupal_array_get_nested_value($form, $parents);

  // 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')),
    ),
  );
}

/**
 * Helper function to rebuild the table structure without submitting the form.
 */
function tablefield_rebuild_form($form, &$form_state) {

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

/**
 * Implements hook_theme().
 */
function tablefield_theme() {
  return array(
    'tablefield_view' => array(
      'variables' => array(
        'caption' => NULL,
        'header' => NULL,
        'rows' => NULL,
        'export' => NULL,
        'delta' => NULL,
        'entity_type' => NULL,
        'entity_id' => NULL,
        'field_name' => NULL,
        'langcode' => NULL,
        'formatter' => NULL,
        'sticky' => NULL,
        'attributes' => array(),
      ),
    ),
  );
}

/**
 * Theme function for table view.
 */
function theme_tablefield_view($variables) {
  $id = $variables['entity_type'] . '-' . $variables['entity_id'] . '-' . $variables['field_name'] . '-' . $variables['delta'];
  $attributes = $variables['attributes'] + array(
    'id' => drupal_html_id('tablefield-' . $id),
    'class' => array(
      'tablefield',
      'tablefield-' . $id,
    ),
  );

  // Apply scope property to headers for accessibility.
  if (is_array($variables['header'])) {
    foreach ($variables['header'] as &$header) {
      $header['scope'] = 'col';
    }
  }

  // If the user has access to the csv export option, display it now.
  $export = '';
  if ($variables['export'] && user_access('export tablefield')) {
    $url = sprintf('tablefield/export/%s/%s/%s/%s/%s', $variables['entity_type'], $variables['entity_id'], $variables['field_name'], $variables['langcode'], $variables['delta']);
    $export = '<div id="' . drupal_html_id('tablefield-export-link-' . $id) . '" class="tablefield-export-link">' . l(t('Export Table Data'), $url) . '</div>';
  }

  // Prepare variables for theme_table().
  $theme_variables = array(
    'header' => $variables['header'],
    'rows' => $variables['rows'],
    'attributes' => $attributes,
  );
  if (isset($variables['caption'])) {
    $theme_variables['caption'] = $variables['caption'];
  }
  if (isset($variables['sticky'])) {
    $theme_variables['sticky'] = $variables['sticky'];
  }
  return '<div id="' . drupal_html_id('tablefield-wrapper-' . $id) . '" class="tablefield-wrapper">' . theme('table__tablefield', $theme_variables) . $export . '</div>';
}

/**
 * Helper function to detect and convert strings not in UTF-8 to UTF-8.
 *
 * @param string $data
 *   The string which needs converting.
 * @param string $encoding
 *   The encoding of the CSV file.
 *
 * @return string
 *   UTF encoded string.
 */
function tablefield_convert_encoding($data, $encoding) {

  // Converting UTF-8 to UTF-8 will not work.
  if ($encoding == 'UTF-8') {
    return $data;
  }

  // Try convert the data to UTF-8.
  if ($encoded_data = drupal_convert_to_utf8($data, $encoding)) {
    return $encoded_data;
  }

  // Fallback on the input data.
  return $data;
}

/**
 * Trim trailing empty rows/columns.
 *
 * @param array $tabledata
 *   The rationalized tablefield.
 * @param bool $ignore_head
 *   Whether ignoring header or not.
 */
function tablefield_trim($tabledata, $ignore_head = FALSE) {
  $tabledata = array_reverse($tabledata);

  // For columns the transposed array has the 'weight' one level higher.
  unset($tabledata['weight']);
  foreach ($tabledata as $key => $value) {

    // Removes the weight key for the rows.
    unset($value['weight']);
    $empty = TRUE;
    if (is_array($value)) {
      foreach ($value as $k2 => $v2) {
        if (!empty($v2)) {

          // Stop traversing at the first non empty value.
          if (!$ignore_head || $ignore_head && $k2 !== 'row_0') {
            $empty = FALSE;
          }
        }
      }
    }
    else {
      if (!empty($value)) {

        // Stop traversing at the first non empty value.
        $empty = FALSE;
      }
    }
    if ($empty) {
      unset($tabledata[$key]);
    }
    else {
      break;
    }
  }
  $tabledata = array_reverse($tabledata);
  return $tabledata;
}

/**
 * Trim trailing empty columns.
 *
 * @param array $tabledata
 *   The rationalized tablefield.
 * @param bool $ignore_head
 *   Whether ignoring header or not.
 */
function tablefield_rtrim_cols($tabledata, $ignore_head = FALSE) {
  $row_num = count($tabledata);
  if (!$row_num) {
    return $tabledata;
  }

  // Transpose the array.
  $tabledata = tablefield_transpose($tabledata);

  // Trim trailing empty rows.
  $tabledata = tablefield_trim($tabledata, $ignore_head);

  // Transpose back.
  $tabledata = tablefield_transpose($tabledata);
  return $tabledata;
}

/**
 * Helper function to transpose table data arrays.
 */
function tablefield_transpose($array) {
  $transposed = array();
  foreach ($array as $key => $row) {
    foreach ($row as $subkey => $value) {
      $transposed[$subkey][$key] = $value;
    }
  }
  return $transposed;
}

/**
 * Hide all empty rows.
 *
 * @param array $tabledata
 *   The rationalized tablefield.
 * @param bool $ignore_head
 *   Whether ignoring header or not.
 */
function tablefield_hide_rows($tabledata, $ignore_head = FALSE) {
  foreach ($tabledata as $key => $value) {

    // Removes the weight key.
    unset($value['weight']);
    $empty = TRUE;
    if (is_array($value)) {
      foreach ($value as $k2 => $v2) {
        if (!empty($v2)) {

          // Stop traversing at the first non empty value.
          if (!$ignore_head || $ignore_head && $k2 !== 'row_0') {
            $empty = FALSE;
          }
        }
      }
    }
    else {
      if (!empty($value)) {

        // Stop traversing at the first non empty value.
        $empty = FALSE;
      }
    }
    if ($empty) {
      unset($tabledata[$key]);
    }
  }
  return $tabledata;
}

/**
 * Hide all empty columns.
 *
 * @param array $tabledata
 *   The rationalized tablefield.
 * @param bool $ignore_head
 *   Whether ignoring header or not.
 */
function tablefield_hide_cols($tabledata, $ignore_head = FALSE) {
  $row_num = count($tabledata);
  if (!$row_num) {
    return $tabledata;
  }

  // Transpose the array.
  $tabledata = tablefield_transpose($tabledata);

  // Trim trailing empty rows.
  $tabledata = tablefield_hide_rows($tabledata, $ignore_head);

  // Transpose back.
  $tabledata = tablefield_transpose($tabledata);
  return $tabledata;
}

/**
 * Implements hook_multiple_field_remove_button_field_widgets_alter().
 *
 * Enable https://www.drupal.org/project/multiple_fields_remove_button.
 */
function tablefield_multiple_field_remove_button_field_widgets_alter(&$fieldwidgets) {

  // Remove button for the following field type widgets.
  $fieldwidgets = array(
    'tablefield',
  );
}

Functions

Namesort descending Description
tablefield_admin_settings_form Menu callback to prepare administration configuration form.
tablefield_after_build Form #after_build callback for tablefield_field_widget_form().
tablefield_convert_encoding Helper function to detect and convert strings not in UTF-8 to UTF-8.
tablefield_delete_table_values Helper function to remove all values in a particular table.
tablefield_export_csv Menu callback to export a table as a CSV.
tablefield_field_formatter_info Implements hook_field_formatter_info().
tablefield_field_formatter_settings_form Implements hook_field_formatter_settings_form().
tablefield_field_formatter_settings_summary Implements hook_field_formatter_settings_summary().
tablefield_field_formatter_view Implements hook_field_formatter_view().
tablefield_field_info Implements hook_field_info().
tablefield_field_is_empty Implements hook_field_is_empty().
tablefield_field_load Implements hook_field_load().
tablefield_field_prepare_view Implements hook_field_prepare_view().
tablefield_field_presave Implements hook_field_presave().
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_get_table_value Get the property just as it is set in the data. Search API indexing addition.
tablefield_help Implements hook_help().
tablefield_hide_cols Hide all empty columns.
tablefield_hide_rows Hide all empty rows.
tablefield_import_csv Helper function to import data from a CSV file.
tablefield_import_pasted Helper function to import pasted data.
tablefield_item_property_info Define metadata about item properties. Search API indexing addition.
tablefield_menu Implements hook_menu().
tablefield_multiple_field_remove_button_field_widgets_alter Implements hook_multiple_field_remove_button_field_widgets_alter().
tablefield_permission Implements hook_permission().
tablefield_property_info_callback Defines info for the properties of the tablefield field data structure.
tablefield_rebuild_form Helper function to rebuild the table structure without submitting the form.
tablefield_rebuild_form_ajax AJAX callback to rebuild the number of rows/columns.
tablefield_rtrim_cols Trim trailing empty columns.
tablefield_theme Implements hook_theme().
tablefield_transpose Helper function to transpose table data arrays.
tablefield_trim Trim trailing empty rows/columns.
theme_tablefield_view Theme function for table view.
_tablefield_entity_access Helper function: A sort-of copy of entity_access() from entity API.