Index: tablefield.css
===================================================================
--- tablefield.css	(revision 135)
+++ tablefield.css	(working copy)
@@ -6,20 +6,34 @@
   margin: 1px 1px 1px 1px;
   padding: 1px 1px 1px 1px;
 }
+#content-field-edit-form .node-tablefield table .form-text,
+.node-form .node-tablefield table .form-text {
+  width: 85%;
+  border: 0;
+  padding: 0.25em 0.5em;
+}
 #content-field-edit-form .node-tablefield .form-item,
 .node-form .node-tablefield .form-item {
-  float: left;
   margin: 0 0 0 0;
   padding: 0 0 0 0;
 }
 #content-field-edit-form .node-tablefield table,
 .node-form .node-tablefield table {
   width: auto;
+  border: 1px solid #ccc;
 }
+#content-field-edit-form .node-tablefield table tr th,
+.node-form .node-tablefield table tr th {
+  padding: 0.25em 0.5em;
+  color: #000;
+  background: #e7e7e7;
+  border: 1px solid #ccc;
+}
 #content-field-edit-form .node-tablefield table tr td,
 .node-form .node-tablefield table tr td {
   margin: 0 0 0 0;
   padding: 0 0 0 0;
+  border: 1px solid #ccc;
 }
 #content-field-edit-form .node-tablefield .tablefield-rebuild,
 .node-form .node-tablefield .tablefield-rebuild {
Index: tablefield.module
===================================================================
--- tablefield.module	(revision 135)
+++ tablefield.module	(working copy)
@@ -8,6 +8,132 @@
  */
 
 /**
+* Implementation of hook_form_alter().
+*/
+function tablefield_form_alter(&$form, &$form_state, $form_id) {
+  // We need to adjust all the node/add en node/edit forms so that all tablefields are wrapped
+  // in a div with an id that includes the tablefields id and its delta
+  if (stristr($form_id, '_node_form'))
+  {
+    $tablefield_found = false;
+    foreach ($form as $field_id=>$field_content)
+    {
+      // Filter CCK fields
+      if (stristr($field_id, 'field_'))
+      {
+        foreach ($field_content as $delta=>$field_value)
+        {
+          // Only process real deltas for this field (not its properties)
+          if (is_numeric($delta))
+          {
+            if ($field_value['#type'] == 'tablefield')
+            {
+              $tablefield_found = true;
+              $form[$field_id][$delta]['#prefix'] = '<div id="tablefield-ahah-wrapper_'.$field_id.'-'.$delta.'">';
+              $form[$field_id][$delta]['#suffix'] = '</div>';
+              $form[$field_id][$delta]['#pre_render'][] = 'field_tipo_pre_render';
+            }
+          }
+        }
+      }
+    }
+    if ($tablefield_found)
+    {
+      $form['#cache'] = TRUE;
+    }
+  }
+}
+
+function tablefield_ahah_rebuild($fieldname, $delta) {
+  // The form is generated in an include file which we need to include manually.
+  include_once 'modules/node/node.pages.inc';
+  // We're starting in step #3, preparing for #4.
+  $form_state = array('storage' => NULL, 'submitted' => FALSE);
+  $form_build_id = $_POST['form_build_id'];
+  // Step #4.
+  $form = form_get_cache($form_build_id, $form_state);
+
+  // Preparing for #5.
+  $args = $form['#parameters'];
+  $form_id = array_shift($args);
+  $form_state['post'] = $form['#post'] = $_POST;
+  $form['#programmed'] = $form['#redirect'] = FALSE;
+
+  // Step #5.
+  drupal_process_form($form_id, $form, $form_state);
+  // Step #6 and #7 and #8.
+  $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
+
+  // Step #9.
+  $tablefield = $form[$fieldname][$delta];
+  // the tablefield is in a group :( ... find it!
+  if (empty($tablefield))
+  {
+    foreach ($form as $key=>$val)
+    {
+      if (stristr($key, 'group_'))
+      {
+        foreach ($val as $field_key => $field_value)
+        {
+          if ($field_key == $fieldname)
+          {
+            $tablefield = $form[$key][$fieldname][$delta];
+          }
+        }
+      }
+    }
+  }
+
+  unset($tablefield['#prefix'], $tablefield['#suffix']);
+  $output = theme('status_messages').drupal_render($tablefield);
+
+  // Final rendering callback.
+  drupal_json(array('status' => TRUE, 'data' => $output));
+}
+
+function tablefield_csv_export($nid, $field_name, $delta)
+{
+  $node = node_load($nid);
+
+  $tabledata = unserialize($node->{$field_name}[$delta]['value']);
+  $tabledata = tablefield_rationalize_table($tabledata);
+
+  $csv = '';
+
+  foreach ($tabledata['data'] as $row)
+  {
+    $csv .= implode(';', $row);
+    $csv .= "\r\n";
+  }
+
+  header("Content-type: text/csv");
+  header("Content-Disposition: attachment; filename=export-tablefield-$nid-$delta.csv");
+  print $csv;
+  exit();
+}
+
+function tablefield_menu()
+{
+  // @todo: the access functions for these callbacks should be secured better!
+  $items['tablefield/rebuild/%/%'] = array(
+    'page callback'    => 'tablefield_ahah_rebuild',
+    'page arguments'   => array(2,3),
+    'access callback'  => 'user_access',
+    'access arguments' => array('access content'),
+    'type'             => MENU_CALLBACK,
+  );
+  $items['tablefield/export/%/%/%'] = array(
+    'page callback'    => 'tablefield_csv_export',
+    'page arguments'   => array(2,3,4),
+    'access callback'  => 'user_access',
+    'access arguments' => array('access content'),
+    'type'             => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
+/**
  * Implementation of hook_theme().
  */
 function tablefield_theme() {
@@ -75,7 +201,7 @@
       if (!empty($field['cell_processing'])) {
         $columns['format'] = array('type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'views' => FALSE);
       }
-      $columns['value'] = array('type' => 'text');
+      $columns['value'] = array('type' => 'longtext');
       return $columns;
 
     case 'views data':
@@ -107,6 +233,7 @@
     case 'load':
       foreach ($items as $delta => $table) {
         $items[$delta]['tabledata'] = tablefield_rationalize_table(unserialize($table['value']));
+        $items[$delta]['tabledata'] = $items[$delta]['tabledata']['data'];
       }
       break;
     case 'sanitize':
@@ -120,25 +247,23 @@
         }
     
         // Multivalue fields will have one row in the db, so make sure that it isn't empty 
-        if (isset($tabledata)) {
+        if (isset($tabledata['data'])) {
 
           // Run the table body through input filters
-          if (!empty($tabledata)) {
-            foreach ($tabledata as $row_key => $row) {
+          if (!empty($tabledata['data'])) {
+            foreach ($tabledata['data'] as $row_key => $row) {
               foreach ($row as $col_key => $cell) {
                 if (!empty($field['cell_processing'])) {
-                  $tabledata[$row_key][$col_key] = array('data' => check_markup($cell, $table['format']), 'class' => 'row-' . $row_key . ' col-' . $col_key);
+                  $tabledata['data'][$row_key][$col_key] = array('data' => check_markup($cell, $table['format']), 'class' => 'row-' . $row_key . ' col-' . $col_key);
                 }
                 else {
-                  $tabledata[$row_key][$col_key] = array('data' => check_plain($cell), 'class' => 'row-' . $row_key . ' col-' . $col_key);
+                  $tabledata['data'][$row_key][$col_key] = array('data' => check_plain($cell), 'class' => 'row-' . $row_key . ' col-' . $col_key);
                 }
               }
             }
           }
 
-          $header = array_shift($tabledata);
-
-          $items[$delta]['value'] = theme('tablefield_view', $header, $tabledata, $field['field_name'], $delta);
+          $items[$delta]['value'] = theme('tablefield_view', $tabledata, $field['field_name'], $delta);
         }
       }
 
@@ -195,11 +320,242 @@
 /**
  * Theme function for table view 
  */
-function theme_tablefield_view($header, $rows, $field_name, $delta) {
+function theme_tablefield_view($tabledata, $field_name, $delta)
+{
   $class_field_name = str_replace('_', '-', $field_name);
-  return '<div id="tablefield-wrapper-' . $class_field_name . '-' . $delta . '" class="tablefield-wrapper">' . theme('table', $header, $rows, array('id' => 'tablefield-' . $class_field_name . '-' . $delta, 'class' => 'tablefield')) . '</div>';
+
+  // Drupal can not handle WCAG20 rules for tables correctly when using the theme table function
+  //  (at least not if we've made it possible to have multiple rows en cols as headers and certainly not
+  //   when we have made it possible to colspan or rowspan those headercells)
+  // We have to do it manually and try to use as much of Drupal's code that does handle things correctly
+
+  // Build the thead section:
+  // only allow the first row(s) that are marked as headers to be in the thead
+  // from the moment that a row is not marked as header, the thead section should stop
+  if (!empty($tabledata['rows']) && is_array($tabledata['rows']))
+  {
+    $thead = '<thead>';
+    foreach ($tabledata['rows'] as $row_index=>$value)
+    {
+      if ($value != 1) break;
+
+      if (!empty($tabledata['data'][$row_index]) && is_array($tabledata['data'][$row_index]))
+      {
+        if ($row_index % 2 == 0) $thead .= '<tr class="odd">';
+        else  $thead .= '<tr class="even">';
+        foreach ($tabledata['data'][$row_index] as $col_index=>$cell)
+        {
+          // Set colspan or rowspan for this cell or don't show it at all (if required)
+          if (!empty($tabledata['colspans_data'][$row_index][$col_index]))
+          {
+            // 1 indicates that this cell should not be rendered. It is in a colspan and is not the first.
+            // All other numbers indicate the number of cells in the colspan
+            if ($tabledata['colspans_data'][$row_index][$col_index] == 1)
+            {
+              continue;
+            }
+            else
+            {
+              $cell['colspan'] .= $tabledata['colspans_data'][$row_index][$col_index];
+            }
+          }
+          if (!empty($tabledata['rowspans_data'][$row_index][$col_index]))
+          {
+            // 1 indicates that this cell should not be rendered. It is in a rowspan and is not the first.
+            // All other numbers indicate the number of cells in the rowspan
+            if ($tabledata['rowspans_data'][$row_index][$col_index] == 1)
+            {
+              continue;
+            }
+            else
+            {
+              $cell['rowspan'] .= $tabledata['rowspans_data'][$row_index][$col_index];
+            }
+          }
+          // If this cell is marked as header in both the row AND the column and there is 
+          // no data in it: the cell should not be rendered as a header-cell
+          $header = true;
+          if ($tabledata['cols'][$col_index] == 1 && empty($cell['data'])) $header = false;
+          // Force an id on this cell
+          $cell['id'] = _tablefield_get_cell_id($row_index, $col_index);
+          // The data input is a textarea: allow for new lines
+          $cell['data'] = _tablefield_parse_cell_data($cell['data']);
+          $thead .= _theme_table_cell($cell, $header);
+        }
+        $thead .= '</tr>';
+      }
+    }
+    $thead .= '</thead>';
+  }
+
+  // Build the tbody section:
+  // Start building from the first non head row that is found
+  $start_row_index = $row_index;
+  if (!empty($tabledata['rows']) && is_array($tabledata['rows']))
+  {
+    $tbody = '<tbody>';
+    foreach ($tabledata['rows'] as $row_index=>$value)
+    {
+      if ($row_index < $start_row_index) continue;
+
+      if (!empty($tabledata['data'][$row_index]) && is_array($tabledata['data'][$row_index]))
+      {
+        if ($row_index % 2 == 0) $tbody .= '<tr class="odd">';
+        else  $tbody .= '<tr class="even">';
+        foreach ($tabledata['data'][$row_index] as $col_index=>$cell)
+        {
+          // Set colspan or rowspan for this cell or don't show it at all (if required)
+          if (!empty($tabledata['colspans_data'][$row_index][$col_index]))
+          {
+            // 1 indicates that this cell should not be rendered. It is in a colspan and is not the first.
+            // All other numbers indicate the number of cells in the colspan
+            if ($tabledata['colspans_data'][$row_index][$col_index] == 1)
+            {
+              continue;
+            }
+            else
+            {
+              $cell['colspan'] .= $tabledata['colspans_data'][$row_index][$col_index];
+            }
+          }
+          if (!empty($tabledata['rowspans_data'][$row_index][$col_index]))
+          {
+            // 1 indicates that this cell should not be rendered. It is in a rowspan and is not the first.
+            // All other numbers indicate the number of cells in the rowspan
+            if ($tabledata['rowspans_data'][$row_index][$col_index] == 1)
+            {
+              continue;
+            }
+            else
+            {
+              $cell['rowspan'] .= $tabledata['rowspans_data'][$row_index][$col_index];
+            }
+          }
+          // If the row or column for this cell has been marked as header, render as a header-cell
+          $header = false;
+          if ($tabledata['cols'][$col_index] == 1 || $tabledata['rows'][$row_index] == 1 ) $header = true;
+          if (!$header)
+          {
+            // Add extra classes if this cell is the first body cell after the thead
+            if ($tabledata['rows'][$row_index-1] == 1) $cell['class'] .= ' first_data_cell_row';
+            // Also add an extra class if this cell is the first after a header column
+            if ($tabledata['cols'][$col_index-1] == 1) $cell['class'] .= ' first_data_cell_col';
+
+            // WCAG20 requires cells to indicate what headings they belong to. This is only required if
+            // one of the cells headings is in a colspan. To save ourselves a headache on calculating that
+            // we will simply always indicate the headers.
+            $cell['headers'] = _tablefield_get_cell_headers($tabledata, $row_index, $col_index);
+          }
+          // Force an id on this cell
+          $cell['id'] = _tablefield_get_cell_id($row_index, $col_index);
+          // The data input is a textarea: allow for new lines
+          $cell['data'] = _tablefield_parse_cell_data($cell['data']);
+          $tbody .= _theme_table_cell($cell, $header);
+        }
+        $tbody .= '</tr>';
+      }
+    }
+    $tbody .= '</tbody>';
+  }
+
+
+  $html = '<div id="tablefield-wrapper-' . $class_field_name . '-' . $delta . '" class="tablefield-wrapper">';
+  $html .= '<table id="tablefield-' . $class_field_name . '-' . $delta.'" class="tablefield">';
+  if (!empty($thead)) $html .= $thead;
+  if (!empty($tbody)) $html .= $tbody;
+  $html .= '</table>';
+  $html .= '</div>';
+
+  return $html;
 }
 
+function _tablefield_get_cell_id($row_index, $col_index)
+{
+  return 'cell_row_'.$row_index.'_col_'.$col_index;
+}
+
+function _tablefield_get_cell_headers(&$tabledata, $row_index, $col_index)
+{
+  $headers = array();
+
+  // Get the id from each header-cell for this cell as indicated by the header-rows
+  if (!empty($tabledata['rows']) && is_array($tabledata['rows']))
+  {
+    foreach ($tabledata['rows'] as $key=>$value)
+    {
+      if ($value == 1)
+      {
+        // If this header cell is part of a colspan, we need to reset the header cell id to the id of the
+        // cell actually defining the colspan. If the header cell IS that cell, we don't have to do anything.
+        if (isset($tabledata['colspans_data'][$key][$col_index]))
+        {
+          if ($tabledata['colspans_data'][$key][$col_index] == 1)
+          {
+            // Start going back in the column index untill we find the starting cell
+            $span_col_index = $col_index-1;
+            while ($tabledata['colspans_data'][$key][$span_col_index] < 1)
+            {
+              $span_col_index--;
+            }
+            $headers[] = _tablefield_get_cell_id($key, $span_col_index);
+          }
+          else
+          {
+            $headers[] = _tablefield_get_cell_id($key, $col_index);
+          }
+
+        }
+        else
+        {
+          $headers[] = _tablefield_get_cell_id($key, $col_index);
+        }
+      }
+    }
+  }
+
+  // Get the id from each header-cell for this cell as indicated by the header-columns
+  if (!empty($tabledata['cols']) && is_array($tabledata['cols']))
+  {
+    foreach ($tabledata['cols'] as $key=>$value)
+    {
+      if ($value == 1)
+      {
+        // If this header cell is part of a rowspan, we need to reset the header cell id to the id of the
+        // cell actually defining the rowspan. If the header cell IS that cell, we don't have to do anything.
+        if (isset($tabledata['rowspans_data'][$row_index][$key]))
+        {
+          if ($tabledata['rowspans_data'][$row_index][$key] == 1)
+          {
+            // Start going back in the column index untill we find the starting cell
+            $span_row_index = $row_index-1;
+            while ($tabledata['rowspans_data'][$row_index][$key] < 1)
+            {
+              $span_row_index--;
+            }
+            $headers[] = _tablefield_get_cell_id($span_row_index, $key);
+          }
+          else
+          {
+            $headers[] = _tablefield_get_cell_id($row_index, $key);
+          }
+
+        }
+        else
+        {
+          $headers[] = _tablefield_get_cell_id($row_index, $key);
+        }
+      }
+    }
+  }
+
+  return implode(' ', $headers);
+}
+
+function _tablefield_parse_cell_data($data)
+{
+  return nl2br($data);
+}
+
 /**
  * Implementation of hook_widget_info().
  */
@@ -279,9 +635,13 @@
   $delta = $element['#delta'];
   $field = $form['#field_info'][$element['#field_name']];
 
+  $tabledata = array();
+  $tabledata['data'] = array();
+  $tabledata['rows'] = array();
+  $tabledata['cols'] = array();
   if (isset($element['#value']['tablefield'])) {
     // A form was submitted
-    $default_value = tablefield_rationalize_table($element['#value']['tablefield']);
+    $tabledata = tablefield_rationalize_table($element['#value']['tablefield']);
 
     // Catch empty form sumissions for required tablefields
     if ($form_state['submitted'] && $element['#required'] && tablefield_content_is_empty($element['#value'], $field)) {
@@ -290,7 +650,7 @@
   }
   elseif (isset($element['#default_value']['value'])) {
     // Default from database
-    $default_value = tablefield_rationalize_table(unserialize($element['#default_value']['value']));
+    $tabledata = tablefield_rationalize_table(unserialize($element['#default_value']['value']));
   }
   else {
     // Get the widget default value
@@ -298,12 +658,8 @@
     $default_count_rows = $field['widget']['default_value'][0]['tablefield']['count_rows'];
   }
 
-  $description = $element['#description'] ? $element['#description'] . ' ' : '';
-  $description .= t('The first row will appear as the table header.');
-
   $element['tablefield'] = array(
     '#title' => $field['widget']['label'],
-    '#description' => $description,
     '#attributes' => array('id' => 'node-tablefield-' . str_replace('_', '-', $element['#field_name']) .'-'. $delta, 'class' => 'node-tablefield'),
     '#type' => 'fieldset',
     '#tree' => TRUE,
@@ -324,9 +680,9 @@
   }
 
   // Determine how many rows/columns are saved in the data
-  if (!empty($default_value)) {
-    $count_rows = count($default_value);
-    foreach ($default_value as $row) {
+  if (!empty($tabledata['data'])) {
+    $count_rows = count($tabledata['data']);
+    foreach ($tabledata['data'] as $row) {
       $temp_count = count($row);
       if ($temp_count > $count_cols) {
         $count_cols = $temp_count; 
@@ -372,23 +728,66 @@
     ),
   );
 
+  // Allow the user to export a csv file
+  if (is_numeric($form['nid']['#value']) && $form['nid']['#value'] > 0)
+  {
+    $element['tablefield']['export'] = array(
+      '#value' => '<a href="/tablefield/export/'.$form['nid']['#value'].'/'.$field['field_name'].'/'.$delta.'">CSV export</a>',
+    );
+  }
+
+  $field_name_for_id = str_replace('_', '-', $field['field_name']);
   // Render the form table
   $element['tablefield']['a_break'] = array(
     '#type' => 'markup',
-    '#value' => '<table>',
+    '#value' => "<table id=\"{$field_name_for_id}-{$delta}-tablefield\" class=\"tablefield\"><thead><tr>",
   );
+  for ($i = -1; $i < $count_cols; $i++)
+  {
+    if ($i == -1)
+    {
+      $element['tablefield']['col_'.$i] = array(
+        '#type' => 'markup',
+        '#value' => '<th class="row_0 col_0">'.t('Head').'</th>',
+      );
+    }
+    else
+    {
+      $element['tablefield']['col_'.$i] = array(
+        '#type' => 'checkbox',
+        '#title' => str_replace('@', '', chr(64 + (intval($i / 26) % 26)) . chr(65 + ($i % 26))),
+        '#prefix' => '<th class="row_0 col_'.$i.'">',
+        '#suffix' => '</th>',
+        '#default_value' => $tabledata['cols'][$i],
+        '#attributes' => array('class' => 'row_0 col_'.$i.' node-tablefield-table-'.$delta),
+      );
+    }
+  };
+  $element['tablefield']['a_break_' . ($count_cols + 1)] = array(
+    '#type' => 'markup',
+    '#value' => '</tr></thead><tbody>',
+  );
   for ($i = 0; $i < $count_rows; $i++) {
     $element['tablefield']['b_break' . $i] = array(
       '#type' => 'markup',
       '#value' => '<tr>',
     );
+    $element['tablefield']['row_'.$i] = array(
+      '#type' => 'checkbox',
+      '#title' => ($i+1),
+      '#prefix' => '<th class="row_'.$i.' col_0">',
+      '#suffix' => '</th>',
+      '#default_value' => $tabledata['rows'][$i],
+      '#attributes' => array('class' => 'row_0 col_'.$i.' node-tablefield-table-'.$delta),
+    );
     for ($ii = 0; $ii < $count_cols; $ii++) {
       $element['tablefield']['cell_' . $i . '_' . $ii] = array(
-        '#type' => 'textfield',
-        '#size' => 10,
+        '#type' => 'textarea',
+        '#rows' => 3,
+        '#cols' => 10,
         '#attributes' => array('id' => 'tablefield-' . str_replace('_', '-', $element['#field_name']) .'-'. $delta . '-cell-' . $i . '-' . $ii),
-        '#default_value' => (empty($field_value)) ? $default_value[$i][$ii] : $field_value,
-        '#prefix' => '<td>',
+        '#default_value' => $tabledata['data'][$i][$ii],
+        '#prefix' => '<td class="row_'.$i.' col_'.$ii.'">',
         '#suffix' => '</td>',
       );
     }
@@ -399,7 +798,7 @@
   }
   $element['tablefield']['t_break' . $i] = array(
     '#type' => 'markup',
-    '#value' => '</table>',
+    '#value' => '</tbody></table>',
   );
 
   // Allow the user to add more rows/columns
@@ -409,7 +808,6 @@
     '#size' => 5,
     '#prefix' => '<div class="clear-block">',
     '#suffix' => '</div>',
-    //'#default_value' => $count_cols,
     '#value' => $count_cols,
   );
   $element['tablefield']['count_rows'] = array(
@@ -418,15 +816,41 @@
     '#size' => 5,
     '#prefix' => '<div class="clear-block">',
     '#suffix' => '</div>',
-    //'#default_value' => $count_rows,
     '#value' => $count_rows,
   );
+
   $element['tablefield']['rebuild'] = array(
     '#type' => 'button',
     '#value' => t('Rebuild Table'),
     '#attributes' => array('class' => 'tablefield-rebuild'),
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+    '#ahah' => array(
+      'path' => 'tablefield/rebuild/'.$field['field_name'].'/'.$delta,
+      'wrapper' => 'tablefield-ahah-wrapper_'.$field['field_name'].'-'.$delta,
+      'event' => 'click',
+      'method' => 'replace',
+    ),
   );
 
+  //Rowspans and colspans
+  $element['tablefield']['colspans'] = array(
+    '#title' => t('Span cells over multiple columns'),
+    '#type' => 'textarea',
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+    '#value' => $tabledata['colspans'],
+    '#description' => t('Enter one span per line and separate the cells with a comma. For example: A1,B1. The data in the first cell will be used. The data in other spanned cells will be ignored!'),
+  );
+  $element['tablefield']['rowspans'] = array(
+    '#title' => t('Span cells over multiple rows'),
+    '#type' => 'textarea',
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+    '#value' => $tabledata['rowspans'],
+    '#description' => t('Enter one span per line and separate the cells with a comma. For example: A1,A2,A3. The data in the first cell will be used. The data in other spanned cells will be ignored!'),
+  );
+
   if (!empty($field['cell_processing'])) {
     $filter_key = $element['#columns'][0];
     $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT;
@@ -437,22 +861,148 @@
   return $element;
 }
 
-function tablefield_rationalize_table($tablefield) {
+function tablefield_rationalize_table($tablefield)
+{
   $count_cols = $tablefield['count_cols'];
   unset($tablefield['count_cols']);
   $count_rows = $tablefield['count_rows'];
   unset($tablefield['count_rows']);
   unset($tablefield['rebuild']);
+
   // Rationalize the table data
-  if (!empty($tablefield)) {
-  foreach ($tablefield as $key => $value) {
-    preg_match('/cell_(.*)_(.*)/', $key, $cell);
-    // $cell[1] is row count $cell[2] is col count
-    if ((int) $cell[1] < $count_rows && $cell['2'] < $count_cols) {
-      $tabledata[$cell[1]][$cell[2]] = $value;
+  if (!empty($tablefield))
+  {
+    $tabledata['colspans_data'] = array();
+    $tabledata['rowspans_data'] = array();
+    $tabledata['data'] = array();
+    foreach ($tablefield as $key => $value)
+    {
+      if (stristr($key, 'row_'))
+      {
+        $tabledata['rows'][str_replace('row_', '', $key)] = $value;
+      }
+      elseif (stristr($key, 'col_'))
+      {
+        $tabledata['cols'][str_replace('col_', '', $key)] = $value;
+      }
+      elseif (stristr($key, 'colspans') || stristr($key, 'rowspans'))
+      {
+        // make the input data available to the form
+        if (stristr($key, 'rowspans'))
+        {
+          $tabledata['rowspans'] = $value;
+        }
+        if (stristr($key, 'colspans'))
+        {
+          $tabledata['colspans'] = $value;
+        }
+
+        // Construct an array indicating what cells are in a pan. The array contains a normal table
+        // structure ( array( [rowindex] => array ( [colindex] => value ). The value indicates the span:
+        //   + 0 (or if the entry does not exists) indicates no span
+        //   + 1 indicates that this cell is in a span, but not the first cell (it should NOT be rendered)
+        //   + any other number indicates that this cell is the start of a span and contains the data. It
+        //     should be rendered and the number indicates how many cells to span.
+        $value = explode("\n", $value);
+        if (!empty($value) && is_array($value))
+        {
+          foreach ($value as $span)
+          {
+            $span = explode(',', $span);
+            if (!empty($span) && is_array($span))
+            {
+              foreach ($span as $idx=>$cell_span_info)
+              {
+                // each $cell_span_info contains the data for 1 cell in the span (ex. C1 or BD24)
+                // Split them into single characters an construct the row number and column number while
+                // iterating over the characters. Constructing the row number is straight-forward, just
+                // paste the numbers after each other. For the columns it's a bit more complex. The
+                // letters need to be translated to there position in the alfabet and multiplied by
+                // their position in the string. (see comments further in the code)
+                $chars = preg_split('//', trim($cell_span_info), -1, PREG_SPLIT_NO_EMPTY);
+                if (!empty($chars) && is_array($chars))
+                {
+                  $row = '';
+                  $col = array();
+                  foreach ($chars as $charkey=>$charval)
+                  {
+                    if (is_numeric($charval))
+                    {
+                      // simply paste the numbers after eachother and we will construct the row number
+                      $row .= $charval;
+                    }
+                    else
+                    {
+                      // The characters are translated to their position in the alfabet and added to an array
+                      $col[] = ord($charval)-64;
+                    }
+                  }
+
+                  $col = array_reverse($col);
+                  // To calculate the column number we need to... (see example :) ):
+                  //   example cell BC34: we only have the BC translated in our array
+                  //      B = position 2
+                  //      C = position 3
+                  //   Our reversed array contains ( 0=>3, 1=>2 )
+                  //   Only the first element should be summed to the total, all the other elements should be
+                  //   multiplied by 26 and multiplied by their position in the array.
+                  //   So BC is actually column 55 and is calculated by the loop as:
+                  //    iteration 1: 0 + 3            = 3
+                  //    iteration 2: 3 + (2 * 1 * 26) = 55
+                  $col_result = 0;
+                  foreach ($col as $colkey=>$colval)
+                  {
+                    if ($colkey == 0)
+                    {
+                      $col_result += $colval;
+                    }
+                    else
+                    {
+                      $col_result += $colval * $colkey * 26;
+                    }
+                  }
+
+                  // Add the number of cells in the span for the first cell or 2 for all other cells in
+                  // the span to the global array. To have WCAG20 compliance easily implemented we keep this
+                  // data available in different arrays and don't directly implement it in the attributes
+                  // array of the celldata array.
+                  if (!empty($row) && !empty($col_result))
+                  {
+                    // rows and cols in the data start with index 0, these calculated values start at 1...
+                    $row_index = $row-1;
+                    $col_index = $col_result-1;
+
+                    if (stristr($key, 'rowspans'))
+                    {
+                      if ($idx == 0) $tabledata['rowspans_data'][$row_index][$col_index] = count($span);
+                      else $tabledata['rowspans_data'][$row_index][$col_index] = 1;
+                    }
+                    if (stristr($key, 'colspans'))
+                    {
+                      if ($idx == 0) $tabledata['colspans_data'][$row_index][$col_index] = count($span);
+                      else $tabledata['colspans_data'][$row_index][$col_index] = 1;
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+      elseif (stristr($key, 'cell_'))
+      {
+        preg_match('/cell_(.*)_(.*)/', $key, $cell);
+        // $cell[1] is row count $cell[2] is col count
+        if ((int) $cell[1] < $count_rows && $cell['2'] < $count_cols)
+        {
+          // For some reason $cell[1] can be an empty string and this will constantly add
+          // a new row to the table when saving...
+          // @todo: figure out why blank row numbers are occuring and fix it
+          if ($cell[1] != '') $tabledata['data'][$cell[1]][$cell[2]] = $value;
+        }
+      }
     }
   }
-  }
 
   return $tabledata;
 }
@@ -478,15 +1028,16 @@
 
     if (is_object($file)) {
       if (($handle = fopen($file->filepath, "r")) !== FALSE) {
-
+        $encoding = mb_detect_encoding($csv);
         tablefield_delete_table_values($form_state['values'][$field_name][$delta]['tablefield']);
 
         // Populate CSV values
         $max_col_count = 0;
         $row_count = 0;
-        while (($csv = fgetcsv($handle, 0, ",")) !== FALSE) {
+        while (($csv = fgetcsv($handle, 0, ";")) !== FALSE) {
           $col_count = count($csv);
           foreach ($csv as $col_id => $col) {
+            if ($encoding != 'UTF-8') $col = utf8_encode($col);
             $form_state['values'][$field_name][$delta]['tablefield']['cell_' . $row_count . '_' . $col_id] = $col;
           }
           $max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
@@ -504,6 +1055,10 @@
       else {
         drupal_set_message(t('There was a problem importing @file.', array('@file' => $file->filename)));
       }
+      if ($encoding != 'UTF-8')
+      {
+        drupal_set_message(t('The csv file has incorrect character encoding. Please verify the imported data.'), 'error');
+      }
     }
 
     // Remove the temporary file
@@ -524,5 +1079,3 @@
     }
   }
 }
-
-
