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' => '', + ); + } + else + { + $element['tablefield']['col_'.$i] = array( + '#type' => 'checkbox', + '#title' => str_replace('@', '', chr(64 + (intval($i / 26) % 26)) . chr(65 + ($i % 26))), + '#prefix' => '', + '#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' => '', + '#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' => '', ); } @@ -399,7 +663,7 @@ } $element['tablefield']['t_break' . $i] = array( '#type' => 'markup', - '#value' => '
'.t('Head').'', + '#suffix' => '
', + '#suffix' => '', + '#default_value' => $tabledata['data'][$i][$ii], + '#prefix' => '', '#suffix' => '
', + '#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 @@ } } } - -