You are here

tablefield.module in TableField 7.3

Same filename and directory in other branches
  1. 6 tablefield.module
  2. 7 tablefield.module
  3. 7.2 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'),
      'description' => t('Acts in addition to the field widget settings (AND).'),
    ),
    'always use additional datasources' => array(
      'title' => t('Always allow additional data sources to be used (CSV upload or copy/paste)'),
      'description' => t('Overrides the field widget settings (OR).'),
    ),
    'rebuild tablefield' => array(
      'title' => t('Rebuild any tablefield'),
      'description' => t('Overrides the field widget settings (OR).'),
    ),
    '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) {

  // Ensure this is a tablefield.
  $field_info = field_info_field($field_name);
  if (!$field_info || $field_info['type'] != 'tablefield') {
    return drupal_not_found();
  }

  // Attempt to load the entity.
  $entities = entity_load($entity_type, array(
    $entity_id,
  ));
  if (empty($entities)) {
    return MENU_NOT_FOUND;
  }
  $entity = reset($entities);

  // Ensure the user has access to view this field on this entity.
  if (!_tablefield_entity_access('view', $entity_type, $entity) || !field_access('view', $field_info, $entity_type, $entity)) {
    return MENU_ACCESS_DENIED;
  }
  $filename = sprintf('%s_%s_%s_%s_%s.csv', $entity_type, $entity_id, $field_name, $langcode, $delta);
  $uri = 'temporary://' . $filename;

  // 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;
    }
  }
  elseif ($entity_type == 'fieldable_panels_pane') {
    return fieldable_panels_panes_access('view', $entity);
  }
  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',
  );
  $properties['value'] = array(
    'type' => 'text',
    'label' => t('The value column of the table.'),
    'computed' => TRUE,
    'getter callback' => 'tablefield_value_array_get',
  );
  return $properties;
}

/**
 * Translate tablefield to associative array.
 *
 * @param array $trat
 *   The tablefield.
 *
 * @return array
 *   The associative array.
 */
function tablefield_tablefield_to_array($trat) {
  $ttrans = array();
  $rowkey = 0;
  foreach ($trat as $rowix => $rowvals) {
    foreach ($rowvals as $ix => $val) {
      $ttrans[$rowkey][] = $val;
    }
    $rowkey++;
  }
  return $ttrans;
}

/**
 * Get the value property in formatted array.
 */
function tablefield_value_array_get($data, array $options, $name, $type, $info) {
  if (isset($data['tabledata']['tabledata'])) {
    $data['value'] = tablefield_tablefield_to_array($data['tabledata']['tabledata']);
  }
  elseif (isset($data['tablefield']['tabledata'])) {
    $data['value'] = tablefield_tablefield_to_array($data['tablefield']['tabledata']);
  }
  return $data['value'];
}

/**
 * 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']['tabledata'])) {
    $data['value'] = '';
    foreach ($data['tabledata']['tabledata'] as $rows) {
      $data['value'] .= implode(" ", $rows) . " ";
    }
  }
  return trim($data['value']);
}

/**
 * Implements hook_field_widget_settings_form().
 */
function tablefield_field_widget_settings_form($field, $instance) {
  $form = array();
  $form['restrict_rebuild'] = array(
    '#type' => 'checkbox',
    '#title' => t('Restrict rebuilding'),
    '#description' => t('Avoid the number of cols/rows being changed by content editors. For all users in combination with locked cells below. For users without the permission "rebuild tablefield" in other cases.') . '<br />' . t('Also needed if added or removed rows from the default field settings should apply on the edit form of existing content.'),
    '#default_value' => isset($instance['widget']['settings']['restrict_rebuild']) ? $instance['widget']['settings']['restrict_rebuild'] : FALSE,
  );
  $form['lock_values'] = array(
    '#type' => 'checkbox',
    '#title' => t('Lock cells with default values'),
    '#description' => t('Avoid headers being changed by content editors.'),
    '#default_value' => isset($instance['widget']['settings']['lock_values']) ? $instance['widget']['settings']['lock_values'] : FALSE,
  );
  $form['input_type'] = array(
    '#type' => 'radios',
    '#title' => t('Input type'),
    '#default_value' => isset($instance['widget']['settings']['input_type']) ? $instance['widget']['settings']['input_type'] : 'textfield',
    '#required' => TRUE,
    '#options' => array(
      'textfield' => t('textfield'),
      'textarea' => t('textarea'),
    ),
  );
  $form['max_length'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum cell length'),
    '#default_value' => isset($instance['widget']['settings']['max_length']) ? $instance['widget']['settings']['max_length'] : '2048',
    '#element_validate' => array(
      'tablefield_validate_number',
    ),
    '#size' => 6,
    '#maxlength' => 6,
    '#min' => 1,
    '#step' => 1,
    '#field_suffix' => t('characters'),
    '#attributes' => array(
      'class' => array(
        'tablefield-form-align',
      ),
    ),
    '#required' => TRUE,
  );
  $form['cell_processing'] = array(
    '#type' => 'radios',
    '#title' => t('Table cell processing'),
    '#default_value' => isset($instance['widget']['settings']['cell_processing']) ? $instance['widget']['settings']['cell_processing'] : 0,
    '#options' => array(
      t('Plain text'),
      t('Filtered text (user selects input format)'),
    ),
  );
  $form['data_sources'] = array(
    '#type' => 'checkboxes',
    '#title' => 'Additional data sources',
    '#description' => t('Note this setting gets overridden for users with the permission "always use additional datasources".'),
    '#options' => array(
      'paste' => t('Copy & Paste'),
      'upload' => t('Upload CSV file'),
    ),
    '#default_value' => isset($instance['widget']['settings']['data_sources']) ? $instance['widget']['settings']['data_sources'] : array(
      'paste',
      'upload',
    ),
  );
  return $form;
}

/**
 * Implements hook_field_settings_form().
 */
function tablefield_field_settings_form($field, $instance, $has_data) {
  $multiple_fields_remove_button_module = l(t('Multiple Fields Remove Button module'), 'https://www.drupal.org/project/multiple_fields_remove_button', array(
    'attributes' => array(
      'title' => t('Project page on Drupal.org'),
      'target' => '_blank',
    ),
  ));
  $manage_display = l(t('Manage display'), '/admin/structure/types/manage/' . $instance['bundle'] . '/display', array(
    'attributes' => array(
      'title' => t('More tablefield options'),
    ),
  ));
  $form = array();
  $form['message'] = array(
    '#markup' => t('If multiple values are allowed it is recommended to install and enable the !multiple_fields_remove_button_module. Additional TableField settings are available at !manage_display.', array(
      '!multiple_fields_remove_button_module' => $multiple_fields_remove_button_module,
      '!manage_display' => $manage_display,
    )),
    '#weight' => -100,
  );
  return $form;
}

/**
 * Form element validation handler for #type 'tablefield_number'.
 *
 * Note that #required is validated by _form_validate() already.
 */
function tablefield_validate_number($element, &$form_state) {
  $value = $element['#value'];
  if ($value === '') {
    return;
  }
  $name = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];

  // Ensure the input is numeric.
  if (!is_numeric($value)) {
    form_error($element, t('%name must be a number.', array(
      '%name' => $name,
    )));
    return;
  }

  // Ensure that the input is greater than the #min property, if set.
  if (isset($element['#min']) && $value < $element['#min']) {
    form_error($element, t('%name must be higher or equal to %min.', array(
      '%name' => $name,
      '%min' => $element['#min'],
    )));
  }

  // Ensure that the input is less than the #max property, if set.
  if (isset($element['#max']) && $value > $element['#max']) {
    form_error($element, t('%name must be below or equal to %max.', array(
      '%name' => $name,
      '%max' => $element['#max'],
    )));
  }
  if (isset($element['#step']) && strtolower($element['#step']) != 'any') {

    // Check that the input is an allowed multiple of #step (offset by #min if
    // #min is set).
    $offset = isset($element['#min']) ? $element['#min'] : 0.0;
    if (!tablefield_valid_number_step($value, $element['#step'], $offset)) {
      form_error($element, t('%name is not a multiple of %step.', array(
        '%name' => $name,
        '%step' => $element['#step'],
      )));
    }
  }
}

/**
 * Verifies that a number is a multiple of a given step.
 *
 * The implementation assumes it is dealing with IEEE 754 double precision
 * floating point numbers that are used by PHP on most systems.
 *
 * This is based on the number/range verification methods of webkit.
 *
 * @param float $value
 *   The value that needs to be checked.
 * @param float $step
 *   The step scale factor. Must be positive.
 * @param float $offset
 *   (optional) An offset, to which the difference must be a multiple of the
 *   given step.
 *
 * @return bool
 *   TRUE if no step mismatch has occured, or FALSE otherwise.
 *
 * @see http://opensource.apple.com/source/WebCore/WebCore-1298/html/NumberInputType.cpp
 */
function tablefield_valid_number_step($value, $step, $offset = 0.0) {
  $double_value = (double) abs($value - $offset);

  // The fractional part of a double has 53 bits. The greatest number that could
  // be represented with that is 2^53. If the given value is even bigger than
  // $step * 2^53, then dividing by $step will result in a very small remainder.
  // Since that remainder can't even be represented with a single precision
  // float the following computation of the remainder makes no sense and we can
  // safely ignore it instead.
  if ($double_value / pow(2.0, 53) > $step) {
    return TRUE;
  }

  // Now compute that remainder of a division by $step.
  $remainder = (double) abs($double_value - $step * round($double_value / $step));

  // $remainder is a double precision floating point number. Remainders that
  // can't be represented with single precision floats are acceptable. The
  // fractional part of a float has 24 bits. That means remainders smaller than
  // $step * 2^-24 are acceptable.
  $computed_acceptable_error = (double) ($step / pow(2.0, 24));
  return $computed_acceptable_error >= $remainder || $remainder >= $step - $computed_acceptable_error;
}

/**
 * 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
  if (arg(0) == '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,
        'striping' => TRUE,
        'sortable' => FALSE,
        'hide_header' => FALSE,
        'hide_empty_rows' => FALSE,
        'hide_empty_cols' => FALSE,
        'hide_cols_skip_head' => FALSE,
        'trim_trailing_cols' => FALSE,
        'trim_trailing_rows' => FALSE,
        'table_custom_class_attributes' => 'table',
        'export_csv' => FALSE,
      ),
    ),
    'format_raw' => array(
      'label' => t('Raw data (JSON or XML)'),
      'field types' => array(
        'tablefield',
      ),
      'settings' => array(
        'tabledatakey' => 'tabledata',
        'usearraykeys' => 'No',
        'rowkey' => FALSE,
        'vertheader' => FALSE,
        'tabledataonly' => TRUE,
        'numeric_check' => TRUE,
        'xml' => FALSE,
        'xml_safe' => 'cdata',
      ),
    ),
  );
}

/**
 * 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('Wrapper for table data (if applicable): %tr', array(
        '%tr' => $settings['tabledatakey'],
      ));
      $summary[] = t('Use first row/column values as keys (if not empty): %tr', array(
        '%tr' => $settings['usearraykeys'],
      ));
      if ($settings['usearraykeys'] === 'Both') {
        $summary[] = t('Row identifier key: %tr', array(
          '%tr' => t('Disabled'),
        ));
      }
      else {
        $summary[] = t('Row identifier key: %tr', array(
          '%tr' => $settings['rowkey'] ? t('Yes') : t('No'),
        ));
      }
      $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'),
      ));
      if ($settings['xml']) {
        $summary[] = t('Encode numeric strings as numbers: %tr', array(
          '%tr' => t('Disabled'),
        ));
      }
      else {
        $summary[] = t('Encode numeric strings as numbers: %tr', array(
          '%tr' => $settings['numeric_check'] ? t('Yes') : t('No'),
        ));
      }
      $summary[] = t('XML instead of JSON: %tr', array(
        '%tr' => $settings['xml'] ? t('Yes') : t('No'),
      ));
      if (!$settings['xml']) {
        $summary[] = t('How to make field values XML safe?: %tr', array(
          '%tr' => t('Disabled'),
        ));
      }
      else {
        $summary[] = t('How to make field values XML safe?: %tr', array(
          '%tr' => $settings['xml_safe'],
        ));
      }
      break;
    default:
      $summary[] = t('Sticky header: %tr', array(
        '%tr' => $settings['sticky_header'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Striping (odd/even class): %tr', array(
        '%tr' => $settings['striping'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Sortable: %tr', array(
        '%tr' => $settings['sortable'] ? t('Yes') : t('No'),
      ));
      $summary[] = t('Hide first row: %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'),
        ),
      ));
      $table_custom_class_attributes = isset($settings['table_custom_class_attributes']) ? $settings['table_custom_class_attributes'] : NULL;
      $summary[] = t('Add custom class attribute classes to table html element: %tr', array(
        '%tr' => $table_custom_class_attributes,
      ));
      $summary[] = t('Show link to export table data as CSV depending on !permission: %tr', array(
        '%tr' => $settings['export_csv'] ? 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':

      // Get a machine name from the field name (without 'field_' prefix).
      $prefix = 'field_';
      $machine = $instance['field_name'];
      $select = 'select';
      $select_or_other_module = l(t('Select (or other) module'), 'https://drupal.org/project/select_or_other', array(
        'attributes' => array(
          'title' => t('Select (or other) | Drupal.org'),
          'target' => '_blank',
        ),
      ));
      $description = t('To provide a custom value install and enable the !select_or_other_module.', array(
        '!select_or_other_module' => $select_or_other_module,
      ));
      if (module_exists('select_or_other')) {
        $select = 'select_or_other';
        $description = NULL;
      }
      if (substr($machine, 0, strlen($prefix)) == $prefix) {
        $machine = substr($machine, strlen($prefix));
      }
      $element['tabledatakey'] = array(
        '#type' => $select,
        '#title' => t('Wrapper for table data (if applicable)'),
        '#description' => $description,
        '#options' => array(
          'tabledata' => t('tabledata (fixed string)'),
          $instance['label'] => t('Label: @label', array(
            '@label' => $instance['label'],
          )),
          $machine => t('Machine name: @machine', array(
            '@machine' => $machine,
          )),
        ),
        '#default_value' => $settings['tabledatakey'],
        '#other_unknown_defaults' => 'other',
        '#other_delimiter' => FALSE,
      );
      $element['usearraykeys'] = array(
        '#type' => 'select',
        '#title' => t('Use first row/column values as 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['rowkey'] = array(
        '#title' => t('Row identifier key'),
        '#type' => 'checkbox',
        '#default_value' => $settings['rowkey'],
      );
      if ($settings['usearraykeys'] === 'Both') {
        $element['rowkey']['#disabled'] = TRUE;
        $element['rowkey']['#title'] = '<span class="grayed-out">' . t('Row identifier key') . '</span> | ' . t("Disabled because above the first column values are set to be used as row identifiers.");
      }
      $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'],
      );
      $element['numeric_check'] = array(
        '#title' => t('Encode numeric strings as numbers'),
        '#type' => 'checkbox',
        '#default_value' => $settings['numeric_check'],
      );
      if ($settings['xml']) {
        $element['numeric_check']['#disabled'] = TRUE;
        $element['numeric_check']['#title'] = '<span class="grayed-out">' . t('Encode numeric strings as numbers') . '</span> | ' . t("Disabled because below XML is selected that does not use quotes around any values.");
      }
      $element['xml'] = array(
        '#title' => t('XML instead of JSON'),
        '#type' => 'checkbox',
        '#default_value' => $settings['xml'],
      );
      $element['xml_safe'] = array(
        '#type' => 'select',
        '#title' => t('How to make field values XML safe?'),
        '#options' => array(
          'htmlspecialchars' => t('Convert special characters to HTML entities (htmlspecialchars)'),
          'cdata' => t('Represent field values that contain special characters as a CDATA section'),
          'cdata_all' => t('Represent all field values as a CDATA section'),
        ),
        '#default_value' => $settings['xml_safe'],
      );
      if (!$settings['xml']) {
        $element['xml_safe']['#disabled'] = TRUE;
        $element['xml_safe']['#title'] = '<span class="grayed-out">' . t('How to make field values XML safe?') . '</span> | ' . t("Disabled because above XML is not selected.");
      }
      break;
    default:
      $element['sticky_header'] = array(
        '#title' => t('Sticky header'),
        '#type' => 'checkbox',
        '#default_value' => $settings['sticky_header'],
      );
      $element['striping'] = array(
        '#title' => t('Striping (odd/even class)'),
        '#type' => 'checkbox',
        '#default_value' => $settings['striping'],
      );
      $element['sortable'] = array(
        '#title' => t('Sortable'),
        '#type' => 'checkbox',
        '#default_value' => $settings['sortable'],
      );
      $tablesorter_module = l(t('Tablesorter module'), 'https://drupal.org/project/tablesorter', array(
        'attributes' => array(
          'title' => t('Tablesorter | Drupal.org'),
          'target' => '_blank',
        ),
      ));
      $element['hide_header'] = array(
        '#title' => t('Hide first row'),
        '#type' => 'checkbox',
        '#default_value' => $settings['hide_header'],
      );
      $element['hide_cols_skip_head'] = array(
        '#title' => t('Hide empty columns ignoring column header'),
        '#description' => t('This will remove the table field completely if all columns are empty including the field label.'),
        '#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'],
      );
      $permission = l(t('permission'), 'admin/people/permissions', array(
        'fragment' => 'module-tablefield',
        'attributes' => array(
          'title' => t('Manage user permissions'),
        ),
      ));
      $element['table_custom_class_attributes'] = array(
        '#title' => t('Add custom class attribute classes to the table html element'),
        '#type' => 'textfield',
        '#size' => 40,
        '#default_value' => $settings['table_custom_class_attributes'],
      );
      $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'];
  if (!empty($items)) {
    switch ($display['type']) {
      case 'format_raw':
        $values = array();
        foreach ($items as $delta => $table) {
          $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']);

          // DEVELOPERS! Extra future data should be captured and unset below.
          $caption = $value['caption'];
          unset($value['caption']);
          if ($settings['usearraykeys'] === 'Header' || $settings['usearraykeys'] === 'Both') {

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

            // Remove linebreaks from array keys.
            array_walk_recursive($keys, function (&$key) {
              $key = str_replace(array(
                "\r",
                "\n",
                "<br />",
              ), '', $key);
            });

            // 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;
                $row_id = str_replace(array(
                  "\r",
                  "\n",
                  "<br />",
                ), '', $row_id);

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

          // For a row identifier key to be retained we need to insert a
          // numeric index key for each row.
          if ($settings['usearraykeys'] === 'Both' || $settings['rowkey']) {
            $i = 0;
            foreach ($value as $key => $val) {
              $value[$i][$key] = $val;

              // Remove original key. We'll get it back with array_values().
              unset($value[$key]);
              $i++;
            }
          }
          if ($settings['tabledataonly']) {
            $value = reset($value);
          }
          else {

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

          // Use the set wrapper for the table data.
          if (isset($value[0]['tabledata']) && $settings['tabledatakey'] !== 'tabledata') {
            $value[0][$settings['tabledatakey']] = $value[0]['tabledata'];
            unset($value[0]['tabledata']);
          }
          elseif (isset($value['tabledata']) && $settings['tabledatakey'] !== 'tabledata') {
            $value[$settings['tabledatakey']] = $value['tabledata'];
            unset($value['tabledata']);
          }

          // Add the table to the final output.
          if ($settings['tabledataonly']) {
            array_push($values, array_values($value));
          }
          else {
            $value[$settings['tabledatakey']] = $settings['usearraykeys'] === 'Both' || $settings['rowkey'] ? $value[0][$settings['tabledatakey']] : array_values($value[$settings['tabledatakey']]);
            unset($value[0]);
            krsort($value);
            array_push($values, $value);
          }

          // If the field is not plain text and a text area then remove
          // linebreaks.
          $format = isset($items[$delta]['format']) ? $items[$delta]['format'] : 'plain_text';
          array_walk_recursive($values, function (&$value) use ($format, $settings) {
            if ($format !== 'plain_text') {
              if ($settings['xml']) {
                switch ($settings['xml_safe']) {
                  case 'htmlspecialchars':
                    $value = str_replace(array(
                      "\r",
                      "\n",
                    ), '', htmlspecialchars($value));
                    break;
                  case 'cdata':
                    if (preg_match('/[&"\'<>]/', $value)) {
                      $value = str_replace(array(
                        "\r",
                        "\n",
                      ), '', '<![CDATA[' . str_replace(']]>', '', $value) . ']]>');
                    }
                    else {
                      $value = str_replace(array(
                        "\r",
                        "\n",
                      ), '', $value);
                    }
                    break;
                  default:
                    $value = str_replace(array(
                      "\r",
                      "\n",
                    ), '', '<![CDATA[' . str_replace(']]>', '', $value) . ']]>');
                    break;
                }
              }
              else {
                $value = str_replace(array(
                  "\r",
                  "\n",
                ), '', $value);
              }
            }
          });
        }

        // Serve pretty print with a fixed-width font and preserve spaces and
        // line breaks.
        // Show pretty print on node pages and raw on other pages.
        $prettyprint = menu_get_object() && arg(2) !== 'themeless' ? JSON_PRETTY_PRINT : FALSE;
        if ($prettyprint) {
          $element['#prefix'] = '<pre>';
          $element['#suffix'] = '</pre>';
        }
        $numeric_check = $settings['numeric_check'] ? JSON_NUMERIC_CHECK : FALSE;

        // Avoid a wrapping array if we have a single table (not multi-value).
        if (!(count($values) - 1)) {
          $values = array_shift($values);
        }

        // Render multiple values as one to ensure valid JSON (e.g. seperator)
        // or XML.
        if ($settings['xml']) {

          // Creating object of SimpleXMLElement.
          $xml_data = new SimpleXMLElement('<?xml version="1.0"?><' . $settings['tabledatakey'] . '></' . $settings['tabledatakey'] . '>');

          // Flatten the array if we only have one element.
          if (count($values) === 1) {
            $values = current($values);
          }

          // Function call to convert array to xml.
          array_to_xml($values, $xml_data);
          if ($prettyprint) {
            $dom = dom_import_simplexml($xml_data)->ownerDocument;
            $dom->formatOutput = TRUE;
            $output = $dom
              ->saveXML();
            $markup = '<xmp>' . $output . '</xmp>';

            // Let other modules change the output.
            $context = array(
              'entity_type' => $entity_type,
              'entity' => $entity,
              'field' => $field,
              'instance' => $instance,
              'language' => $langcode,
              'items' => $items,
              'display' => $display,
              'table_field_output_type' => 'xml_pretty_print',
            );
            drupal_alter('tablefield_output', $markup, $context);
            $element[0] = array(
              '#markup' => $markup,
            );
          }
          else {
            $output = $xml_data
              ->asXML();
            $markup = str_replace('<?xml version="1.0"?>', '', $output);

            // Let other modules change the output.
            $context = array(
              'entity_type' => $entity_type,
              'entity' => $entity,
              'field' => $field,
              'instance' => $instance,
              'language' => $langcode,
              'items' => $items,
              'display' => $display,
              'table_field_output_type' => 'xml_raw',
            );
            drupal_alter('tablefield_output', $markup, $context);
            $element[0] = array(
              '#markup' => $markup,
            );
          }
        }
        else {
          $markup = json_encode($values, $prettyprint | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | $numeric_check);

          // Let other modules change the output.
          $context = array(
            'entity_type' => $entity_type,
            'entity' => $entity,
            'field' => $field,
            'instance' => $instance,
            'language' => $langcode,
            'items' => $items,
            'display' => $display,
            'table_field_output_type' => $prettyprint ? 'json_pretty_print' : 'json_raw',
          );
          drupal_alter('tablefield_output', $markup, $context);
          $element[0] = array(
            // See http://php.net/manual/en/json.constants.php#119565.
            '#markup' => check_plain($markup),
          );
        }
        break;
      default:
        foreach ($items as $delta => $table) {

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

            // Grab the header orientation.
            $header_orientation = !empty($tabledata['rebuild']['header_orientation']) ? $tabledata['rebuild']['header_orientation'] : (isset($instance['default_value'][0]['tablefield']['rebuild']['header_orientation']) ? $instance['default_value'][0]['tablefield']['rebuild']['header_orientation'] : 'Horizontal');

            // 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 ? [] : $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.
            if (empty($settings['hide_header']) && $header_data || $settings['hide_header']) {
              array_shift($tabledata);
            }

            // Theme the table for display, but only if we have printable
            // table data.
            if (!$settings['hide_cols_skip_head'] || $tabledata || $header) {
              $element[$delta] = array(
                '#theme' => 'tablefield_view',
                '#caption' => $caption,
                '#header_orientation' => $header_orientation,
                '#sticky' => isset($settings['sticky_header']) ? $settings['sticky_header'] : NULL,
                '#striping' => isset($settings['striping']) ? $settings['striping'] : NULL,
                '#sortable' => isset($settings['sortable']) ? $settings['sortable'] : NULL,
                '#header' => $header,
                '#rows' => $tabledata,
                '#delta' => $delta,
                '#table_custom_class_attributes' => !trim($settings['table_custom_class_attributes']) == FALSE ? $settings['table_custom_class_attributes'] : NULL,
                '#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,
              );
            }
          }
        }
    }
  }
  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_field_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';
  if ($settings) {
    $hide_header = isset($settings['hide_header']) ? $settings['hide_header'] : FALSE;
  }

  /* 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_size['header_orientation'] = isset($instance['default_value'][0]['tablefield']['rebuild']['header_orientation']) ? $instance['default_value'][0]['tablefield']['rebuild']['header_orientation'] : 'Horizontal';

  // 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);
      $identifier = implode('--', $tablefield_parents);
      $form_state['_tablefield_imported_values'][$identifier] = $default_value;
    }
    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 {

    // If this is data that was saved before 'header_orientation' was a
    // per-value setting, then we need to keep the valuef from $default_size.
    if (empty($default_value['rebuild']['header_orientation'])) {
      $default_value['rebuild']['header_orientation'] = $default_size['header_orientation'];
    }
    $default_size = $default_value['rebuild'];
  }
  $count_rows = $default_size['count_rows'];
  $count_cols = $default_size['count_cols'];
  $header_orientation = $default_size['header_orientation'];

  // Now we can build the widget.
  $help_text = '';
  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 ($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 ($hide_header) {
      $element['tablefield']['#attributes']['class'][] = 'table-no-headers';
    }
  }
  if (in_array($header_orientation, array(
    'Horizontal',
    'Both',
  ))) {
    $element['tablefield']['#attributes']['class'][] = 'tablefield-header-orientation-horizontal';
  }
  if (in_array($header_orientation, array(
    'Vertical',
    'Both',
  ))) {
    $element['tablefield']['#attributes']['class'][] = 'tablefield-header-orientation-vertical';
  }

  // 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>';
  }
  if (arg(0) == '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 (!$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;

  // Make added default values visible when editing existing content.
  if (isset($instance['widget']['settings']['restrict_rebuild']) && $instance['widget']['settings']['restrict_rebuild'] && $instance['widget']['settings']['lock_values'] && arg(0) !== 'admin' && arg(0) !== 'system') {

    // Use the default number of cols/rows if they changed.
    $count_cols = $count_cols != $instance['default_value'][0]['tablefield']['rebuild']['count_cols'] ? $instance['default_value'][0]['tablefield']['rebuild']['count_cols'] : $count_cols;
    $count_rows = $count_rows != $instance['default_value'][0]['tablefield']['rebuild']['count_rows'] ? $instance['default_value'][0]['tablefield']['rebuild']['count_rows'] : $count_rows;
  }

  // Loop over all the rows.
  for ($i = 0; $i < $count_rows; $i++) {
    $zebra = $i % 2 == 0 ? 'even' : 'odd';

    // Disable table drag functionality in case of locked default cells under
    // the first row or completely on the field settings form.
    $draggable = 'draggable ';
    unset($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]['weight']);
    if (arg(0) == 'admin' || isset($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]) && array_filter($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"])) {
      $draggable = FALSE;
    }
    $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($instance['widget']['settings']['lock_values']) && arg(0) != '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 class="col-' . $ii . '">',
          '#suffix' => '</td>',
        );
      }
      else {
        $input_type = isset($instance['widget']['settings']['input_type']) ? $instance['widget']['settings']['input_type'] : 'textfield';
        $max_length = isset($instance['widget']['settings']['max_length']) ? abs($instance['widget']['settings']['max_length']) : '2048';
        $cell_default = isset($default_value['row_' . $i]['col_' . $ii]) ? $default_value['row_' . $i]['col_' . $ii] : '';

        // If the field does not contain HTML, is not plain text and a text area
        // then covert linebreaks to <br />.
        $format = isset($items[$delta]['format']) ? $items[$delta]['format'] : 'plain_text';
        if (!($cell_default != strip_tags($cell_default)) && $format !== 'plain_text') {
          $nl2br = array(
            '_cell_validate',
          );
        }
        else {
          $nl2br = NULL;
        }
        $element['tablefield']['tabledata']['row_' . $i]['col_' . $ii] = array(
          '#type' => $input_type,
          '#maxlength' => $max_length,
          '#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 class="col-' . $ii . '">',
          '#suffix' => '</td>',
          '#element_validate' => $nl2br,
        );
      }
    }

    // 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 Assistive Technology, like screen readers, better describe the content within.'),
  );

  // Could be the Paragraphs module.
  if (isset($items[$delta]['tablefield'])) {
    $raw = $items[$delta]['tablefield'];
  }
  elseif (isset($items[$delta]['value'])) {
    $raw = unserialize($items[$delta]['value']);
  }
  elseif ($delta === 0 && isset($instance['default_value'][0]['tablefield'])) {
    $raw = $instance['default_value'][0]['tablefield'];
  }
  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($instance['widget']['settings']['restrict_rebuild']) && $instance['widget']['settings']['restrict_rebuild'] && !user_access('rebuild tablefield') || isset($instance['widget']['settings']['restrict_rebuild']) && $instance['widget']['settings']['restrict_rebuild'] && $instance['widget']['settings']['lock_values'] && arg(0) !== 'admin' && arg(0) !== 'system') {
    $element['tablefield']['rebuild'] = array(
      '#type' => 'value',
      '#tree' => TRUE,
      'count_cols' => array(
        '#type' => 'value',
        '#value' => $count_cols,
      ),
      'count_rows' => array(
        '#type' => 'value',
        '#value' => $count_rows,
      ),
      'header_orientation' => array(
        '#type' => 'value',
        '#value' => $header_orientation,
      ),
      'rebuild' => array(
        '#type' => 'value',
        '#value' => t('Rebuild Table'),
      ),
    );
  }
  else {
    $element['tablefield']['rebuild'] = array(
      '#type' => 'fieldset',
      '#tree' => TRUE,
      '#title' => t('Number of rows/columns and header orientation'),
      '#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']['header_orientation'] = array(
      '#type' => 'select',
      '#title' => t('Header orientation'),
      '#options' => array(
        'Horizontal' => t('First row (horizontal)'),
        'Vertical' => t('First column (vertical)'),
        'Both' => t('Both first row and first column (two headers, horizontal and vertical)'),
      ),
      '#default_value' => $header_orientation,
    );
    $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',
      ),
    );
  }
  if (user_access('always use additional datasources') || isset($instance['widget']['settings']['data_sources']['upload']) && $instance['widget']['settings']['data_sources']['upload'] === 'upload') {

    // 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',
      ),
    );
  }
  if (user_access('always use additional datasources') || isset($instance['widget']['settings']['data_sources']['paste']) && $instance['widget']['settings']['data_sources']['paste'] === 'paste') {

    // 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($instance['widget']['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;
    }
  }
  $form['element']['#element_validate'][] = 'tablefield_field_widget_form_validate';
  return $element;
}

/**
 * Form validation callback.
 *
 * If the POST request exceeds the server's limit of max_input_vars, the
 * tablefield values that were computed by a CSV import will be directly get set
 * by the CSV table, bypassing the form submission step, and also any changes
 * that the user made on browser.
 */
function tablefield_field_widget_form_validate(&$form, &$form_state, &$element) {
  if (tablefield_post_exceeds_max_input_vars()) {

    // Check if the values have been imported from a CSV file. If so, original
    // values still exist in $form_state['_tablefield_imported_values'].
    $values_from_csv = FALSE;
    if (isset($form_state['_tablefield_imported_values'])) {
      $values_from_csv = $form_state['_tablefield_imported_values'];
    }
    if ($values_from_csv) {

      // Each key is one of the fields multiple values.
      $elements = array_keys($values_from_csv);
      foreach ($elements as $key) {
        $table_values = $values_from_csv[$key];

        // Reconstruct in the same form as the array is passed in $form_state.
        $table_values_format = array();
        $i_row = 0;
        foreach ($table_values as $row) {
          $i_col = 0;
          foreach ($row as $cell) {
            $table_values_format['cell_' . $i_row . '_' . $i_col] = $cell;
            $i_col++;
          }
          $i_row++;
        }
        $keys = explode('--', $key);
        tablefield_replace_csv_values($form_state['values'], $keys, $table_values_format);
        $message_warning = t('This table is too large to be safely edited. The original CSV values are stored. Please import a new CSV file to make changes to this table.');
        drupal_set_message($message_warning, 'warning');
      }
    }
    else {
      $message_error = t('This table is too large to be safely edited. To assure that no data is lost, please import a new CSV file to make changes to this table.');
      drupal_set_message($message_error, 'error');
    }
  }
}

/**
 * Replace table field values with the values that were saved.
 *
 * Helper function that uses recursion to replace table field values from
 * $form_state['values'] with the values that were saved right after the csv
 * upload.
 *
 * The function is used if the size of the table is so large, that on the form
 * submission the max_input_vars is exceeded.
 *
 * @param array $destination
 *   It should only contain the $form_state['values'].
 * @param array $keys
 *   Array with the nested keys of the field's location into
 *   $form_state['values'].
 * @param array $value
 *   The table to be placed.
 */
function tablefield_replace_csv_values(array &$destination, array &$keys, array $value) {
  $cur_key = array_shift($keys);
  if (!isset($destination[$cur_key])) {
    return;
  }
  if (!empty($keys)) {
    tablefield_replace_csv_values($destination[$cur_key], $keys, $value);
    return;
  }
  foreach ($value as $key => $value2) {
    $destination[$cur_key][$key] = $value2;
  }
}

/**
 * Implements hook_custom_theme().
 */
function tablefield_custom_theme() {

  // Ensure that if this is a valid POST request that we use the same theme
  // used by the referring form.
  if (isset($_POST['form_build_id']) && path_is_admin($_GET['q']) && user_access('view the administration theme')) {
    return variable_get('admin_theme');
  }
}

/**
 * Custom callback for a textarea to be processed for linebreaks.
 */
function _cell_validate($element, &$form_state) {
  $value = $element['#value'];
  if (!empty($element['#value'])) {
    $value = nl2br($value);
    form_set_value($element, $value, $form_state);
  }
}

/**
 * Form #after_build callback for tablefield_field_widget_form().
 */
function tablefield_after_build($form, &$form_state) {
  drupal_add_css(drupal_get_path('module', 'tablefield') . '/css/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 = str_getcsv($row, $col_delimiter);
      $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,
        'header_orientation' => 'Horizontal',
        'sticky' => NULL,
        'striping' => NULL,
        'sortable' => NULL,
        'table_custom_class_attributes' => NULL,
        'attributes' => array(),
      ),
    ),
    'tablefield' => array(
      'variables' => array(
        'rows' => NULL,
        'attributes' => array(),
        'caption' => NULL,
        'colgroups' => array(),
        'header_orientation' => 'Horizontal',
        'sticky' => NULL,
        'striping' => NULL,
        'sortable' => NULL,
        'table_custom_class_attributes' => NULL,
        'empty' => NULL,
      ),
    ),
  );
}

/**
 * Theme function for table view.
 */
function theme_tablefield_view($variables) {
  $id = $variables['entity_type'] . '-' . $variables['entity_id'] . '-' . $variables['field_name'] . '-' . $variables['delta'];
  $sortable = $variables['sortable'] ? 'tablesorter' : NULL;
  $cols_class = isset($variables['header']) ? 'tablefield-columns-' . count($variables['header']) : NULL;
  $attributes = array(
    'id' => 'tablefield-' . $id,
    'class' => array(
      'tablefield',
      $sortable,
      $cols_class,
    ),
  );

  // 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_tablefield().
  $theme_variables = array(
    'header' => $variables['header'],
    'rows' => $variables['rows'],
    'attributes' => $attributes,
    'colgroups' => array(),
    'empty' => NULL,
  );
  if (isset($variables['caption'])) {
    $theme_variables['caption'] = $variables['caption'];
  }
  if (isset($variables['header_orientation'])) {
    $theme_variables['header_orientation'] = $variables['header_orientation'];
  }
  if (isset($variables['sticky'])) {
    $theme_variables['sticky'] = $variables['sticky'];
  }
  if (isset($variables['striping'])) {
    $theme_variables['striping'] = $variables['striping'];
  }
  if (isset($variables['sortable'])) {
    $theme_variables['sortable'] = $variables['sortable'];
  }
  if (!trim($variables['table_custom_class_attributes']) == FALSE) {
    $theme_variables['attributes']['class'][] = $variables['table_custom_class_attributes'];
  }
  return '<div id="' . drupal_html_id('tablefield-wrapper-' . $id) . '" class="tablefield-wrapper">' . theme('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 = !empty($tabledata) ? count($tabledata) : 0;
  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 = !empty($tabledata) ? count($tabledata) : 0;
  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[] = 'tablefield';
}

/**
 * Implements hook_form_alter().
 *
 * Avoid empty tables on multivalue fields with default header values.
 */
function tablefield_form_alter(&$form, &$form_state, $form_id) {
  if (empty($form_state['field'])) {
    return;
  }
  foreach (element_children($form_state['field']) as $field_name) {
    foreach ($form_state['field'][$field_name] as $lang => $value) {
      if (isset($value['instance']) && $value['instance']['widget']['type'] === 'tablefield' && $value['field']['cardinality'] != 1) {
        $key_exists = FALSE;
        $max_delta = $form[$field_name][$lang]['#max_delta'];
        $parents = array_merge($value['array_parents'], array(
          $field_name,
          $lang,
        ));
        $element =& drupal_array_get_nested_value($form, $parents, $key_exists);
        if ($key_exists && isset($element[$max_delta])) {
          unset($element[$max_delta]);
        }
      }
    }
  }
}

/**
 * Returns HTML for a table. Based on the core function theme_table().
 *
 * @param array $variables
 *   An associative array containing:
 *   - header: An array containing the table headers. Each element of the array
 *     can be either a localized string or an associative array with the
 *     following keys:
 *     - "data": The localized title of the table column.
 *     - "field": The database field represented in the table column (required
 *       if user is to be able to sort on this column).
 *     - "sort": A default sort order for this column ("asc" or "desc"). Only
 *       one column should be given a default sort order because table sorting
 *       only applies to one column at a time.
 *     - Any HTML attributes, such as "colspan", to apply to the column header
 *       cell.
 *   - rows: An array of table rows. Every row is an array of cells, or an
 *     associative array with the following keys:
 *     - "data": an array of cells
 *     - Any HTML attributes, such as "class", to apply to the table row.
 *     - "no_striping": a boolean indicating that the row should receive no
 *       'even / odd' styling. Defaults to FALSE.
 *     Each cell can be either a string or an associative array with the
 *     following keys:
 *     - "data": The string to display in the table cell.
 *     - "header": Indicates this cell is a header.
 *     - Any HTML attributes, such as "colspan", to apply to the table cell.
 *     Here's an example for $rows:.
 *
 * @code
 *     $rows = array(
 *       // Simple row
 *       array(
 *         'Cell 1', 'Cell 2', 'Cell 3'
 *       ),
 *       // Row with attributes on the row and some of its cells.
 *       array(
 *         'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky')
 *       )
 *     );
 * @endcode
 *   - attributes: An array of HTML attributes to apply to the table tag.
 *   - caption: A localized string to use for the <caption> tag.
 *   - colgroups: An array of column groups. Each element of the array can be
 *     either:
 *     - An array of columns, each of which is an associative array of HTML
 *       attributes applied to the COL element.
 *     - An array of attributes applied to the COLGROUP element, which must
 *       include a "data" attribute. To add attributes to COL elements, set the
 *       "data" attribute with an array of columns, each of which is an
 *       associative array of HTML attributes.
 *     Here's an example for $colgroup:
 * @code
 *     $colgroup = array(
 *       // COLGROUP with one COL element.
 *       array(
 *         array(
 *           'class' => array('funky'), // Attribute for the COL element.
 *         ),
 *       ),
 *       // Colgroup with attributes and inner COL elements.
 *       array(
 *         'data' => array(
 *           array(
 *             'class' => array('funky'), // Attribute for the COL element.
 *           ),
 *         ),
 *         'class' => array('jazzy'), // Attribute for the COLGROUP element.
 *       ),
 *     );
 * @endcode
 *     These optional tags are used to group and set properties on columns
 *     within a table. For example, one may easily group three columns and
 *     apply same background style to all.
 *   - header_orientation: Horizontal, vertical or both.
 *   - sticky: Use a "sticky" table header.
 *   - striping: Do add an 'odd' or 'even' class to rows.
 *   - sortable: Allow column sorting by clicking the column in the  first row.
 *   - empty: The message to display in an extra row if table does not have any
 *     rows.
 */
function theme_tablefield($variables) {
  $header = $variables['header'];
  $rows = $variables['rows'];
  $attributes = $variables['attributes'];
  $caption = $variables['caption'];
  $colgroups = $variables['colgroups'];
  $header_orientation = $variables['header_orientation'];
  $sticky = $variables['sticky'];
  $table_custom_class_attributes = $variables['table_custom_class_attributes'];
  $no_striping = !$variables['striping'];
  $empty = $variables['empty'];
  if ($variables['sortable']) {
    drupal_add_css(drupal_get_path('module', 'tablefield') . '/css/tablefield_sort.css');
    $tooltip = ' title="' . t('Sort column') . '"';
  }
  else {
    $tooltip = NULL;
  }
  drupal_add_css(drupal_get_path('module', 'tablefield') . '/css/tablefield_sort.css');
  $empty = $variables['empty'];

  // If the only header is the first column make the first row a normal row.
  if (!isset($rows['row_0']) && $header_orientation == 'Vertical') {
    $rows['row_0'] = $header;
    ksort($rows);
    $header = array();
  }

  // Add sticky headers, if applicable.
  if (!empty($header) && $sticky) {
    drupal_add_js('misc/tableheader.js');

    // Add 'sticky-enabled' class to the table to identify it for JS.
    // This is needed to target tables constructed by this function.
    $attributes['class'][] = 'sticky-enabled';
  }
  $output = '<table' . drupal_attributes($attributes) . ">\n";
  if (isset($caption)) {
    $output .= '<caption>' . $caption . "</caption>\n";
  }

  // Format the table columns:
  if (!empty($colgroups)) {
    foreach ($colgroups as $number => $colgroup) {
      $attributes = array();

      // Check if we're dealing with a simple or complex column.
      if (isset($colgroup['data'])) {
        foreach ($colgroup as $key => $value) {
          if ($key == 'data') {
            $cols = $value;
          }
          else {
            $attributes[$key] = $value;
          }
        }
      }
      else {
        $cols = $colgroup;
      }

      // Build colgroup.
      if (is_array($cols) && !empty($cols)) {
        $output .= ' <colgroup' . drupal_attributes($attributes) . '>';
        $i = 0;
        foreach ($cols as $col) {
          $output .= ' <col' . drupal_attributes($col) . ' />';
        }
        $output .= " </colgroup>\n";
      }
      else {
        $output .= ' <colgroup' . drupal_attributes($attributes) . " />\n";
      }
    }
  }

  // Add the 'empty' row message if available.
  if (empty($rows) && $empty) {
    $header_count = 0;
    foreach ($header as $header_cell) {
      if (is_array($header_cell)) {
        $header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1;
      }
      else {
        $header_count++;
      }
    }
    $rows[] = array(
      array(
        'data' => $empty,
        'colspan' => $header_count,
        'class' => array(
          'empty',
          'message',
        ),
      ),
    );
  }

  // Format the table header:
  if (!empty($header)) {
    $ts = tablesort_init($header);

    // HTML requires that the thead tag has tr tags in it followed by tbody
    // tags. Using ternary operator to check and see if we have any rows.
    $output .= !empty($rows) ? ' <thead' . $tooltip . '><tr>' : ' <tr>';
    foreach ($header as $cell) {
      $cell = tablesort_header($cell, $header, $ts);
      $output .= _theme_table_cell($cell, TRUE);
    }

    // Using ternary operator to close the tags based on whether or not there
    // are rows.
    $output .= !empty($rows) ? " </tr></thead>\n" : "</tr>\n";
  }
  else {
    $ts = array();
  }

  // Format the table rows:
  if (!empty($rows)) {
    $output .= "<tbody>\n";
    $flip = array(
      'even' => 'odd',
      'odd' => 'even',
    );
    $class = 'even';
    foreach ($rows as $number => $row) {

      // Check if we're dealing with a simple or complex row.
      if (isset($row['data'])) {
        $cells = $row['data'];
        $no_striping = isset($row['no_striping']) ? $row['no_striping'] : FALSE;

        // Set the attributes array and exclude 'data' and 'no_striping'.
        $attributes = $row;
        unset($attributes['data']);
        unset($attributes['no_striping']);
      }
      else {
        $cells = $row;
        $attributes = array();
      }
      if (!empty($cells)) {

        // Add odd/even class.
        if (!$no_striping) {
          $class = $flip[$class];
          $attributes['class'][] = $class;
        }

        // Build row.
        $output .= ' <tr' . drupal_attributes($attributes) . '>';
        $i = 0;
        foreach ($cells as $cell) {
          $cell = tablesort_cell($cell, $header, $ts, $i++);
          $output .= _theme_table_cell($cell, ($header_orientation === 'Both' || $header_orientation === 'Vertical') && $i == 1);
          $i++;
        }
        $output .= " </tr>\n";
      }
    }
    $output .= "</tbody>\n";
  }
  $output .= "</table>\n";
  return $output;
}

/**
 * Convert array to XML. See http://stackoverflow.com/a/5965940/523688.
 */
function array_to_xml($data, &$xml_data) {
  foreach ($data as $key => $value) {
    if (is_numeric($key)) {

      // Dealing with <0/>..<n/> issues.
      $key = 'item_' . $key;
    }

    // Within keys (XML element tags) only accept A-Z (case insensitive), 0-9,
    // a dash (-) and an underscore (_). Replace all others with an underscore.
    // Avoid multiple consecutive underscores.
    $key = preg_replace('/_+/', '_', preg_replace('/[^A-Za-z0-9_\\-]/', '_', $key));

    // If the <3element> starts with a number, prefix it with an underscore
    // <_3element> to get valid XML.
    $key = is_numeric(substr($key, 0, 1)) ? substr_replace($key, '_', 0, 0) : $key;
    if (is_array($value)) {
      $subnode = $xml_data
        ->addChild($key);
      array_to_xml($value, $subnode);
    }
    else {
      $xml_data
        ->addChild("{$key}", htmlspecialchars("{$value}"));
    }
  }
}

/**
 * Check if $_POST contains more input variables than max_input_vars.
 *
 * @return bool
 *   TRUE if $_POST request variable contains more input variables than the
 *   max_input_vars configuration on the server. FALSE otherwise.
 */
function tablefield_post_exceeds_max_input_vars() {
  $posted_vars = count($_POST, COUNT_RECURSIVE);
  $max_input_vars = ini_get('max_input_vars');
  return $posted_vars > $max_input_vars;
}

Functions

Namesort descending Description
array_to_xml Convert array to XML. See http://stackoverflow.com/a/5965940/523688.
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_custom_theme Implements hook_custom_theme().
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_field_widget_form().
tablefield_field_widget_form_validate Form validation callback.
tablefield_field_widget_info Implements hook_field_widget_info().
tablefield_field_widget_settings_form Implements hook_field_widget_settings_form().
tablefield_form_alter Implements hook_form_alter().
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_post_exceeds_max_input_vars Check if $_POST contains more input variables than max_input_vars.
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_replace_csv_values Replace table field values with the values that were saved.
tablefield_rtrim_cols Trim trailing empty columns.
tablefield_tablefield_to_array Translate tablefield to associative array.
tablefield_theme Implements hook_theme().
tablefield_transpose Helper function to transpose table data arrays.
tablefield_trim Trim trailing empty rows/columns.
tablefield_validate_number Form element validation handler for #type 'tablefield_number'.
tablefield_valid_number_step Verifies that a number is a multiple of a given step.
tablefield_value_array_get Get the value property in formatted array.
theme_tablefield Returns HTML for a table. Based on the core function theme_table().
theme_tablefield_view Theme function for table view.
_cell_validate Custom callback for a textarea to be processed for linebreaks.
_tablefield_entity_access Helper function: A sort-of copy of entity_access() from entity API.