tablefield.module in TableField 7
Same filename and directory in other branches
This module provides a set of fields that can be used to store tabular data with a node. The implementation uses a custom CCK widget.
File
tablefield.moduleView source
<?php
/**
* @file
* This module provides a set of fields that can be used to store
* tabular data with a node. The implementation uses a custom CCK widget.
*/
/**
* @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_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' => 'default',
),
);
}
/**
* Implements hook_field_settings_form().
*/
function tablefield_field_settings_form($field, $instance, $has_data) {
$form = array();
$form['lock_values'] = array(
'#type' => 'checkbox',
'#title' => 'Lock table field defaults from further edits during node add/edit.',
'#default_value' => isset($field['settings']['lock_values']) ? $field['settings']['lock_values'] : FALSE,
);
$form['cell_processing'] = array(
'#type' => 'radios',
'#title' => t('Table cell processing'),
'#default_value' => isset($field['settings']['cell_processing']) ? $field['settings']['cell_processing'] : 0,
'#options' => array(
t('Plain text'),
t('Filtered text (user selects input format)'),
),
);
$form['default_message'] = array(
'#type' => 'markup',
'#value' => t('To specify a default table, use the "Default Value" above. There you can specify a default number of rows/columns and values.'),
);
return $form;
}
/**
* Implements hook_field_schema().
*/
function tablefield_field_schema($field) {
return array(
'columns' => array(
'value' => array(
'type' => 'text',
'size' => 'big',
),
'format' => array(
'type' => 'varchar',
'length' => 255,
'default value' => '',
),
),
);
}
/**
* 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();
foreach ($table['tablefield'] as $key => $value) {
$tablefield[$key] = $value;
}
$items[$delta]['value'] = serialize($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) {
drupal_add_css(drupal_get_path('module', 'tablefield') . '/tablefield.css');
// 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 ($items as $delta => $table) {
if (isset($table[0]['value'])) {
$items[$delta][0]['tabledata'] = tablefield_rationalize_table(unserialize($table[0]['value']));
}
}
}
/**
* Implementation of hook_field_is_empty().
*/
function tablefield_field_is_empty($item, $field) {
// @todo, is this the best way to mark the default value form?
// if we don't, it won't save the number of rows/cols
// Allow the system settings form to have an emtpy table
$arg0 = arg(0);
if ($arg0 == 'admin') {
return FALSE;
}
// Remove the preference fields to see if the table cells are all empty
unset($item['tablefield']['rebuild']);
unset($item['tablefield']['import']);
if (!empty($item['tablefield'])) {
foreach ($item['tablefield'] as $cell) {
if (!empty($cell)) {
return FALSE;
}
}
}
return TRUE;
}
/**
* Implementation of hook_field_formatter_info().
*/
function tablefield_field_formatter_info() {
return array(
'default' => array(
'label' => t('Tabular View'),
'field types' => array(
'tablefield',
),
),
);
}
/**
* 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'];
foreach ($items as $delta => $table) {
// Rationalize the stored data
if (!empty($table['tablefield'])) {
$tabledata = tablefield_rationalize_table($table['tablefield']);
}
elseif (!empty($table['value'])) {
$tabledata = tablefield_rationalize_table(unserialize($table['value']));
}
// Run the table through input filters
if (isset($tabledata)) {
if (!empty($tabledata)) {
foreach ($tabledata as $row_key => $row) {
foreach ($row as $col_key => $cell) {
if (!empty($table['format'])) {
$tabledata[$row_key][$col_key] = array(
'data' => check_markup($cell, $table['format']),
'class' => array(
'row_' . $row_key,
'col_' . $col_key,
),
);
}
else {
$tabledata[$row_key][$col_key] = array(
'data' => check_plain($cell),
'class' => array(
'row_' . $row_key,
'col_' . $col_key,
),
);
}
}
}
}
// Pull the header for theming
$header = array_shift($tabledata);
// Theme the table for display
$element[$delta]['#markup'] = theme('tablefield_view', array(
'header' => $header,
'rows' => $tabledata,
'delta' => $delta,
));
}
}
return $element;
}
/**
* Implements hook_field_widget_info().
*/
function tablefield_field_widget_info() {
return array(
'tablefield' => array(
'label' => t('Table field'),
'field types' => array(
'tablefield',
),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
),
);
}
/**
* Implements hook_widget_form().
*/
function tablefield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
drupal_add_css(drupal_get_path('module', 'tablefield') . '/tablefield.css');
$element['#type'] = 'tablefield';
$form['#attributes']['enctype'] = 'multipart/form-data';
// Establish a list of saved/submitted/default values
if (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta) {
// Rebuilding table rows/cols
$default_value = tablefield_rationalize_table($form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']);
}
elseif (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield-import-button-' . $field['field_name'] . '-' . $delta) {
// Importing CSV data
tablefield_import_csv($form, $form_state);
$default_value = tablefield_rationalize_table($form_state['input'][$field['field_name']][$langcode][$delta]['tablefield']);
}
elseif ($form_state['submitted'] && isset($items[$delta]) && isset($items[$delta]['tablefield'])) {
// A form was submitted
$default_value = tablefield_rationalize_table($items[$delta]['tablefield']);
}
elseif (isset($items[$delta]['value'])) {
// Default from database (saved node)
$default_value = tablefield_rationalize_table(unserialize($items[$delta]['value']));
}
else {
// After the first table, we don't want to populate the values in the table
if ($delta > 0) {
tablefield_delete_table_values($instance['default_value'][0]['tablefield']);
}
// Get the widget default value
$default_value = tablefield_rationalize_table($instance['default_value'][0]['tablefield']);
$default_count_cols = isset($items[0]['tablefield']['rebuild']['count_cols']) ? $items[0]['tablefield']['rebuild']['count_cols'] : 5;
$default_count_rows = isset($items[0]['tablefield']['rebuild']['count_cols']) ? $items[0]['tablefield']['rebuild']['count_cols'] : 5;
}
if (!empty($instance['description'])) {
$help_text = $instance['description'];
}
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' => $help_text,
'#attributes' => array(
'id' => 'form-tablefield-' . $field['field_name'] . '-' . $delta,
'class' => array(
'form-tablefield',
),
),
'#type' => 'fieldset',
'#tree' => TRUE,
'#collapsible' => FALSE,
'#prefix' => '<div id="tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper">',
'#suffix' => '</div>',
);
// Give the fieldset the appropriate class if it is required
if ($element['#required']) {
$element['tablefield']['#title'] .= ' <span class="form-required" title="' . t('This field is required') . '">*</span>';
}
$arg0 = arg(0);
if ($arg0 == 'admin') {
$element['tablefield']['#description'] = t('This form defines the table field defaults, but the number of rows/columns and content can be overridden on a per-node basis. The first row will appear as the table header. Leave the first row bland if you do not need a header.');
}
// Determine how many rows/columns are saved in the data
if (!empty($default_value)) {
$count_rows = count($default_value);
$count_cols = 0;
foreach ($default_value as $row) {
$temp_count = count($row);
if ($temp_count > $count_cols) {
$count_cols = $temp_count;
}
}
}
else {
$count_rows = count($default_value);
$count_cols = isset($default_count_cols) ? $default_count_cols : 0;
$count_rows = isset($default_count_rows) ? $default_count_rows : 0;
}
// Override the number of rows/columns if the user rebuilds the form.
if (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta) {
$count_cols = $form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']['rebuild']['count_cols'];
$count_rows = $form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']['rebuild']['count_rows'];
drupal_set_message(t('Table structure rebuilt.'), 'status', FALSE);
}
// Render the form table
$element['tablefield']['a_break'] = array(
'#markup' => '<table>',
);
for ($i = 0; $i < $count_rows; $i++) {
$zebra = $i % 2 == 0 ? 'even' : 'odd';
$element['tablefield']['b_break' . $i] = array(
'#markup' => '<tr class="tablefield-row-' . $i . ' ' . $zebra . '">',
);
for ($ii = 0; $ii < $count_cols; $ii++) {
$instance_default = isset($instance['default_value'][$delta]) ? $instance['default_value'][$delta]['tablefield']["cell_{$i}_{$ii}"] : array();
if (!empty($instance_default) && !empty($field['settings']['lock_values']) && $arg0 != 'admin') {
// The value still needs to be send on every load in order for the
// table to be saved correctly.
$element['tablefield']['cell_' . $i . '_' . $ii] = array(
'#type' => 'value',
'#value' => $instance_default,
);
// Display the default value, since it's not editable.
$element['tablefield']['cell_' . $i . '_' . $ii . '_display'] = array(
'#type' => 'item',
'#title' => $instance_default,
'#prefix' => '<td style="width:' . floor(100 / $count_cols) . '%">',
'#suffix' => '</td>',
);
}
else {
$cell_default = isset($default_value[$i][$ii]) ? $default_value[$i][$ii] : '';
$element['tablefield']['cell_' . $i . '_' . $ii] = array(
'#type' => 'textfield',
'#maxlength' => 2048,
'#size' => 0,
'#attributes' => array(
'id' => 'tablefield_' . $delta . '_cell_' . $i . '_' . $ii,
'class' => array(
'tablefield-row-' . $i,
'tablefield-col-' . $ii,
),
'style' => 'width:100%',
),
'#default_value' => empty($field_value) ? $cell_default : $field_value,
'#prefix' => '<td style="width:' . floor(100 / $count_cols) . '%">',
'#suffix' => '</td>',
);
}
}
$element['tablefield']['c_break' . $i] = array(
'#markup' => '</tr>',
);
}
$element['tablefield']['t_break' . $i] = array(
'#markup' => '</table>',
);
// Allow the user to add more rows/columns
$element['tablefield']['rebuild'] = array(
'#type' => 'fieldset',
'#tree' => TRUE,
'#title' => t('Change number of rows/columns.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$element['tablefield']['rebuild']['count_cols'] = array(
'#title' => t('How many Columns'),
'#type' => 'textfield',
'#size' => 5,
'#prefix' => '<div class="clearfix">',
'#suffix' => '</div>',
'#value' => $count_cols,
);
$element['tablefield']['rebuild']['count_rows'] = array(
'#title' => t('How many Rows'),
'#type' => 'textfield',
'#size' => 5,
'#prefix' => '<div class="clearfix">',
'#suffix' => '</div>',
'#value' => $count_rows,
);
$element['tablefield']['rebuild']['rebuild'] = array(
'#type' => 'button',
'#validate' => array(),
'#limit_validation_errors' => array(),
'#executes_submit_callback' => TRUE,
'#submit' => array(
'tablefield_rebuild_form',
),
'#value' => t('Rebuild Table'),
'#name' => 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta,
'#attributes' => array(
'class' => array(
'tablefield-rebuild',
),
),
'#ajax' => array(
'callback' => 'tablefield_rebuild_form_ajax',
'wrapper' => 'tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper',
'effect' => 'fade',
'event' => 'click',
),
);
// Allow the user to import a csv file
$element['tablefield']['import'] = array(
'#type' => 'fieldset',
'#tree' => TRUE,
'#title' => t('Import from CSV'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$element['tablefield']['import']['tablefield_csv_' . $field['field_name'] . '_' . $delta] = array(
'#title' => 'File upload',
'#type' => 'file',
);
$element['tablefield']['import']['rebuild_' . $field['field_name'] . '_' . $delta] = array(
'#type' => 'button',
'#validate' => array(),
'#limit_validation_errors' => array(),
'#executes_submit_callback' => TRUE,
'#submit' => array(
'tablefield_rebuild_form',
),
'#value' => t('Upload CSV'),
'#name' => 'tablefield-import-button-' . $field['field_name'] . '-' . $delta,
'#attributes' => array(
'class' => array(
'tablefield-rebuild',
),
),
'#ajax' => array(
'callback' => 'tablefield_rebuild_form_ajax',
'wrapper' => 'tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper',
'effect' => 'fade',
'event' => 'click',
),
);
// Allow the user to select input filters
if (!empty($field['settings']['cell_processing'])) {
$element['#base_type'] = $element['#type'];
$element['#type'] = 'text_format';
$element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL;
}
return $element;
}
/**
* Helper function to import data from a CSV file
* @param array $form
* @param array $form_state
*/
function tablefield_import_csv($form, &$form_state) {
// Look for the field name by checking on the clicked button
if (preg_match('/tablefield-import-button-(.*)$/', $form_state['clicked_button']['#name'], $id)) {
// Extract the field and file name from the id of the clicked button
$file_name = preg_replace('/\\-/', '_', $id[1]);
preg_match('/_([0-9]+)$/', $file_name, $field_delta);
// Extract the field delta from the field name
$delta = $field_delta[1];
$field_name = preg_replace('/' . $field_delta[0] . '/', '', $file_name);
$language = isset($form['language']['#value']) ? $form['language']['#value'] : 'und';
$file = file_save_upload($field_name, array(
'file_validate_extensions' => array(
'csv',
),
));
if (is_object($file)) {
if (($handle = fopen($file->uri, "r")) !== FALSE) {
tablefield_delete_table_values($form_state['values'][$field_name][$language][$delta]['tablefield']);
// Populate CSV values
$max_col_count = 0;
$row_count = 0;
while (($csv = fgetcsv($handle, 0, ",")) !== FALSE) {
$col_count = count($csv);
foreach ($csv as $col_id => $col) {
$form_state['input'][$field_name][$language][$delta]['tablefield']['cell_' . $row_count . '_' . $col_id] = $form_state['values'][$field_name][$language][$delta]['tablefield']['cell_' . $row_count . '_' . $col_id] = $col;
}
$max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
$row_count++;
}
fclose($handle);
$form_state['input'][$field_name][$language][$delta]['tablefield']['rebuild']['count_cols'] = $form_state['values'][$field_name][$language][$delta]['tablefield']['rebuild']['count_cols'] = $max_col_count;
$form_state['input'][$field_name][$language][$delta]['tablefield']['rebuild']['count_rows'] = $form_state['values'][$field_name][$language][$delta]['tablefield']['rebuild']['count_rows'] = $row_count;
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 remove all values in a particular table
* @param array $tablefield
*/
function tablefield_delete_table_values(&$tablefield) {
// Empty out previously entered values
foreach ($tablefield as $key => $value) {
if (strpos($key, 'cell_') === 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.
* @param array $form
* @param array $form_state
*/
function tablefield_rebuild_form_ajax($form, $form_state) {
$rebuild = $form;
$parents = $form_state['clicked_button']['#parents'];
if ($form['#id'] == 'field-ui-field-edit-form') {
$rebuild = $form['instance']['default_value_widget'][$parents[0]];
}
else {
foreach ($parents as $parent) {
// Locate the delta of the field - 0 deltas need to break after
// descending to the 'rebuild' element, but deltas greater than
// 0 need to break /before/ adding the 'rebuild' element.
if (is_int($parent)) {
$delta = $parent;
}
$tmp = $rebuild;
if ($parent == 'rebuild' || $parent == 'import') {
$rebuild = $delta == 0 ? $tmp[$parent] : $tmp;
//$rebuild = $tmp[$parent];
break;
}
$rebuild = $tmp[$parent];
}
}
// 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')),
),
);
}
// @todo how do we persist form_state.values for non-node entities????
// - e.g. what is the generic way to do something like node_form_submit_build_node?
// - it appears to work as expected in other entity forms, but keep testing nonetheless
/**
* Helper function to rebuild the table structure w/o submitting the form.
* @param array $form
* @param array $form_state
*/
function tablefield_rebuild_form($form, &$form_state) {
// Maintain the tablefield data.
$form_state['tablefield_rebuild'] = $form_state['input'];
$form_state['rebuild'] = true;
// Maintain the submitted node values. This is similar
// to how the node preview works.
if (isset($form['#node_edit_form'])) {
$node = node_form_submit_build_node($form, $form_state);
}
}
/**
* Helper function to turn form elements into a structured array.
*
* @param array $tablefield
* The table as it appears in FAPI.
*/
function tablefield_rationalize_table($tablefield) {
$tabledata = array();
// Remove exterraneous form data
$count_cols = $tablefield['rebuild']['count_cols'];
$count_rows = $tablefield['rebuild']['count_rows'];
unset($tablefield['rebuild']);
unset($tablefield['import']);
// 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;
}
}
}
return $tabledata;
}
/**
* Implements hook_theme().
*/
function tablefield_theme() {
return array(
'tablefield_view' => array(
'variables' => array(
'header' => NULL,
'rows' => NULL,
'delta' => NULL,
),
),
);
}
/**
* Theme function for table view
*/
function theme_tablefield_view($variables) {
$attributes = array(
'id' => 'tablefield-' . $variables['delta'],
'class' => array(
'tablefield',
),
);
return '<div id="tablefield-wrapper-' . $variables['delta'] . '" class="tablefield-wrapper">' . theme('table', array(
'header' => $variables['header'],
'rows' => $variables['rows'],
'attributes' => $attributes,
)) . '</div>';
}
Functions
Name | Description |
---|---|
tablefield_delete_table_values | Helper function to remove all values in a particular table |
tablefield_field_formatter_info | Implementation of hook_field_formatter_info(). |
tablefield_field_formatter_view | Implements hook_field_formatter_view(). |
tablefield_field_info | Implements hook_field_info(). |
tablefield_field_is_empty | Implementation of hook_field_is_empty(). |
tablefield_field_load | Implements hook_field_load(). |
tablefield_field_presave | Implements hook_field_presave(). |
tablefield_field_schema | Implements hook_field_schema(). |
tablefield_field_settings_form | Implements hook_field_settings_form(). |
tablefield_field_validate | Implements hook_field_validate(). |
tablefield_field_widget_error | Implements hook_field_widget_error(). |
tablefield_field_widget_form | Implements hook_widget_form(). |
tablefield_field_widget_info | Implements hook_field_widget_info(). |
tablefield_import_csv | Helper function to import data from a CSV file |
tablefield_rationalize_table | Helper function to turn form elements into a structured array. |
tablefield_rebuild_form | Helper function to rebuild the table structure w/o submitting the form. |
tablefield_rebuild_form_ajax | 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. |
tablefield_theme | Implements hook_theme(). |
theme_tablefield_view | Theme function for table view |