Index: tablefield.css
===================================================================
--- tablefield.css (revision 123)
+++ 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 123)
+++ tablefield.module (working copy)
@@ -107,6 +107,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 +121,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 +194,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 '
' . theme('table', $header, $rows, array('id' => 'tablefield-' . $class_field_name . '-' . $delta, 'class' => 'tablefield')) . '
';
+
+ // 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 = '';
+ 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 .= '';
+ else $thead .= '
';
+ 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 .= '
';
+ }
+ }
+ $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 = '';
+ 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 .= '';
+ else $tbody .= '
';
+ 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 .= '
';
+ }
+ }
+ $tbody .= '';
+ }
+
+
+ $html = '';
+ $html .= '
';
+ if (!empty($thead)) $html .= $thead;
+ if (!empty($tbody)) $html .= $tbody;
+ $html .= '
';
+ $html .= '
';
+
+ 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 +509,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 +524,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 +532,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 +554,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;
@@ -375,20 +605,54 @@
// Render the form table
$element['tablefield']['a_break'] = array(
'#type' => 'markup',
- '#value' => '',
+ '#value' => "",
);
+ for ($i = -1; $i < $count_cols; $i++)
+ {
+ if ($i == -1)
+ {
+ $element['tablefield']['col_'.$i] = array(
+ '#type' => 'markup',
+ '#value' => ''.t('Head').' | ',
+ );
+ }
+ else
+ {
+ $element['tablefield']['col_'.$i] = array(
+ '#type' => 'checkbox',
+ '#title' => str_replace('@', '', chr(64 + (intval($i / 26) % 26)) . chr(65 + ($i % 26))),
+ '#prefix' => '',
+ '#suffix' => ' | ',
+ '#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' => '
',
+ );
for ($i = 0; $i < $count_rows; $i++) {
$element['tablefield']['b_break' . $i] = array(
'#type' => 'markup',
'#value' => '',
);
+ $element['tablefield']['row_'.$i] = array(
+ '#type' => 'checkbox',
+ '#title' => ($i+1),
+ '#prefix' => '',
+ '#suffix' => ' | ',
+ '#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' => '',
+ '#default_value' => $tabledata['data'][$i][$ii],
+ '#prefix' => ' | ',
'#suffix' => ' | ',
);
}
@@ -399,7 +663,7 @@
}
$element['tablefield']['t_break' . $i] = array(
'#type' => 'markup',
- '#value' => '
',
+ '#value' => '
',
);
// Allow the user to add more rows/columns
@@ -409,7 +673,6 @@
'#size' => 5,
'#prefix' => '',
'#suffix' => '
',
- //'#default_value' => $count_cols,
'#value' => $count_cols,
);
$element['tablefield']['count_rows'] = array(
@@ -418,9 +681,24 @@
'#size' => 5,
'#prefix' => '',
'#suffix' => '
',
- //'#default_value' => $count_rows,
'#value' => $count_rows,
);
+ $element['tablefield']['colspans'] = array(
+ '#title' => t('Span cells over multiple columns'),
+ '#type' => 'textarea',
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#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' => '',
+ '#suffix' => '
',
+ '#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!'),
+ );
$element['tablefield']['rebuild'] = array(
'#type' => 'button',
'#value' => t('Rebuild Table'),
@@ -437,22 +715,146 @@
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();
+ 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 hte 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...
+ if ($cell[1] != '') $tabledata['data'][$cell[1]][$cell[2]] = $value;
+ }
+ }
}
}
- }
return $tabledata;
}
@@ -478,15 +880,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 +907,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 +931,3 @@
}
}
}
-
-