tablefield.module in TableField 7.3
Same filename and directory in other branches
Provides a set of fields that can be used to store tabular data with a node.
@todo Should we create a helper function for sanitization?
- We should see if it makes sense to sanitize on load as well as view.
File
tablefield.moduleView source
<?php
/**
* @file
* Provides a set of fields that can be used to store tabular data with a node.
*
* @todo Should we create a helper function for sanitization?
* - We should see if it makes sense to sanitize on load as well as view.
*/
/**
* Implements hook_help().
*/
function tablefield_help($path, $arg) {
switch ($path) {
case 'admin/help#tablefield':
// Return a line-break version of the README.txt.
return _filter_autop(file_get_contents(dirname(__FILE__) . '/README.txt'));
}
}
/**
* Implements hook_menu().
*/
function tablefield_menu() {
return array(
'tablefield/export/%/%/%/%/%' => array(
'page callback' => 'tablefield_export_csv',
'page arguments' => array(
2,
3,
4,
5,
6,
),
'title' => 'Export Table Data',
'access arguments' => array(
'export tablefield',
),
),
'admin/config/content/tablefield' => array(
'page callback' => 'drupal_get_form',
'page arguments' => array(
'tablefield_admin_settings_form',
),
'title' => 'Tablefield',
'description' => 'Global configuration for the Tablefield module.',
'access arguments' => array(
'configure tablefield',
),
),
);
}
/**
* Menu callback to prepare administration configuration form.
*/
function tablefield_admin_settings_form() {
$form = array();
$form['tablefield_csv_separator'] = array(
'#type' => 'textfield',
'#title' => t('CSV separator'),
'#size' => 1,
'#maxlength' => 1,
'#default_value' => variable_get('tablefield_csv_separator', ','),
'#description' => t('Select the separator for the CSV import/export.'),
);
return system_settings_form($form);
}
/**
* Implements hook_permission().
*/
function tablefield_permission() {
return array(
'export tablefield' => array(
'title' => t('Export Tablefield Data as CSV'),
'description' => t('Acts in addition to the field widget settings (AND).'),
),
'always use additional datasources' => array(
'title' => t('Always allow additional data sources to be used (CSV upload or copy/paste)'),
'description' => t('Overrides the field widget settings (OR).'),
),
'rebuild tablefield' => array(
'title' => t('Rebuild any tablefield'),
'description' => t('Overrides the field widget settings (OR).'),
),
'configure tablefield' => array(
'title' => t('Allow changes in the global tablefield module configuration'),
),
);
}
/**
* Menu callback to export a table as a CSV.
*
* @param string $entity_type
* The type of entity, e.g. node.
* @param string $entity_id
* The id of the entity.
* @param string $field_name
* The machine name of the field to load.
* @param string $langcode
* The language code specified.
* @param string $delta
* The field delta to load.
*/
function tablefield_export_csv($entity_type, $entity_id, $field_name, $langcode, $delta) {
// Ensure this is a tablefield.
$field_info = field_info_field($field_name);
if (!$field_info || $field_info['type'] != 'tablefield') {
return drupal_not_found();
}
// Attempt to load the entity.
$entities = entity_load($entity_type, array(
$entity_id,
));
if (empty($entities)) {
return MENU_NOT_FOUND;
}
$entity = reset($entities);
// Ensure the user has access to view this field on this entity.
if (!_tablefield_entity_access('view', $entity_type, $entity) || !field_access('view', $field_info, $entity_type, $entity)) {
return MENU_ACCESS_DENIED;
}
$filename = sprintf('%s_%s_%s_%s_%s.csv', $entity_type, $entity_id, $field_name, $langcode, $delta);
$uri = 'temporary://' . $filename;
// Ensure that the data is available and that we can load a
// temporary file to stream the data.
if (isset($entity->{$field_name}[$langcode][$delta]['value']) && ($fp = fopen($uri, 'w+'))) {
$table = unserialize($entity->{$field_name}[$langcode][$delta]['value']);
// Save the data as a CSV file.
foreach ($table['tabledata'] as $row) {
// Remove the weight column.
array_pop($row);
fputcsv($fp, $row, variable_get('tablefield_csv_separator', ','));
}
fclose($fp);
// Add basic HTTP headers.
$http_headers = array(
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Content-Length' => filesize($uri),
);
// IE needs special headers.
if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
$http_headers['Cache-Control'] = 'must-revalidate, post-check=0, pre-check=0';
$http_headers['Pragma'] = 'public';
}
else {
$http_headers['Pragma'] = 'no-cache';
}
// Stream the download.
file_transfer($uri, $http_headers);
}
// Something went wrong.
drupal_add_http_header('Status', '500 Internal Server Error');
print t('Error generating CSV.');
drupal_exit();
}
/**
* Helper function: A sort-of copy of entity_access() from entity API.
*
* @todo Remove if entity_access() ends up in core.
*/
function _tablefield_entity_access($op, $entity_type, $entity = NULL, $account = NULL) {
$info = entity_get_info($entity_type);
// Use entity API access callbacks if provided.
if (isset($info[$entity_type]['access callback'])) {
$access_callback = $info[$entity_type]['access callback'];
return $access_callback($op, $entity, $account, $entity_type);
}
elseif ($entity_type == 'node') {
return node_access($op, $entity, $account);
}
elseif ($entity_type == 'user') {
return user_view_access($entity);
}
elseif ($entity_type == 'taxonomy_term') {
if (user_access('administer taxonomy', $account)) {
return TRUE;
}
if (user_access('access content', $account)) {
return TRUE;
}
}
elseif ($entity_type == 'fieldable_panels_pane') {
return fieldable_panels_panes_access('view', $entity);
}
return FALSE;
}
/**
* Implements hook_field_info().
*/
function tablefield_field_info() {
return array(
'tablefield' => array(
'label' => t('Table Field'),
'description' => t('Stores a table of text fields'),
'default_widget' => 'tablefield',
'default_formatter' => 'tablefield_default',
'property_type' => 'tablefield',
'property_callbacks' => array(
'tablefield_property_info_callback',
),
),
);
}
/**
* Defines info for the properties of the tablefield field data structure.
*/
function tablefield_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
$property =& $info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
$property['getter callback'] = 'entity_metadata_field_verbatim_get';
$property['setter callback'] = 'entity_metadata_field_verbatim_set';
// Needed for Search API indexing.
$property['property info'] = tablefield_item_property_info();
unset($property['query callback']);
}
/**
* Define metadata about item properties. Search API indexing addition.
*/
function tablefield_item_property_info() {
$properties['table_value'] = array(
'type' => 'text',
'label' => t('The value of the table.'),
'computed' => TRUE,
'getter callback' => 'tablefield_get_table_value',
);
$properties['value'] = array(
'type' => 'text',
'label' => t('The value column of the table.'),
'computed' => TRUE,
'getter callback' => 'tablefield_value_array_get',
);
return $properties;
}
/**
* Translate tablefield to associative array.
*
* @param array $trat
* The tablefield.
*
* @return array
* The associative array.
*/
function tablefield_tablefield_to_array($trat) {
$ttrans = array();
$rowkey = 0;
foreach ($trat as $rowix => $rowvals) {
foreach ($rowvals as $ix => $val) {
$ttrans[$rowkey][] = $val;
}
$rowkey++;
}
return $ttrans;
}
/**
* Get the value property in formatted array.
*/
function tablefield_value_array_get($data, array $options, $name, $type, $info) {
if (isset($data['tabledata']['tabledata'])) {
$data['value'] = tablefield_tablefield_to_array($data['tabledata']['tabledata']);
}
elseif (isset($data['tablefield']['tabledata'])) {
$data['value'] = tablefield_tablefield_to_array($data['tablefield']['tabledata']);
}
return $data['value'];
}
/**
* Get the property just as it is set in the data. Search API indexing addition.
*/
function tablefield_get_table_value($data, array $options, $name, $type, $info) {
if (isset($data['tabledata']['tabledata'])) {
$data['value'] = '';
foreach ($data['tabledata']['tabledata'] as $rows) {
$data['value'] .= implode(" ", $rows) . " ";
}
}
return trim($data['value']);
}
/**
* Implements hook_field_widget_settings_form().
*/
function tablefield_field_widget_settings_form($field, $instance) {
$form = array();
$form['restrict_rebuild'] = array(
'#type' => 'checkbox',
'#title' => t('Restrict rebuilding'),
'#description' => t('Avoid the number of cols/rows being changed by content editors. For all users in combination with locked cells below. For users without the permission "rebuild tablefield" in other cases.') . '<br />' . t('Also needed if added or removed rows from the default field settings should apply on the edit form of existing content.'),
'#default_value' => isset($instance['widget']['settings']['restrict_rebuild']) ? $instance['widget']['settings']['restrict_rebuild'] : FALSE,
);
$form['lock_values'] = array(
'#type' => 'checkbox',
'#title' => t('Lock cells with default values'),
'#description' => t('Avoid headers being changed by content editors.'),
'#default_value' => isset($instance['widget']['settings']['lock_values']) ? $instance['widget']['settings']['lock_values'] : FALSE,
);
$form['input_type'] = array(
'#type' => 'radios',
'#title' => t('Input type'),
'#default_value' => isset($instance['widget']['settings']['input_type']) ? $instance['widget']['settings']['input_type'] : 'textfield',
'#required' => TRUE,
'#options' => array(
'textfield' => t('textfield'),
'textarea' => t('textarea'),
),
);
$form['max_length'] = array(
'#type' => 'textfield',
'#title' => t('Maximum cell length'),
'#default_value' => isset($instance['widget']['settings']['max_length']) ? $instance['widget']['settings']['max_length'] : '2048',
'#element_validate' => array(
'tablefield_validate_number',
),
'#size' => 6,
'#maxlength' => 6,
'#min' => 1,
'#step' => 1,
'#field_suffix' => t('characters'),
'#attributes' => array(
'class' => array(
'tablefield-form-align',
),
),
'#required' => TRUE,
);
$form['cell_processing'] = array(
'#type' => 'radios',
'#title' => t('Table cell processing'),
'#default_value' => isset($instance['widget']['settings']['cell_processing']) ? $instance['widget']['settings']['cell_processing'] : 0,
'#options' => array(
t('Plain text'),
t('Filtered text (user selects input format)'),
),
);
$form['data_sources'] = array(
'#type' => 'checkboxes',
'#title' => 'Additional data sources',
'#description' => t('Note this setting gets overridden for users with the permission "always use additional datasources".'),
'#options' => array(
'paste' => t('Copy & Paste'),
'upload' => t('Upload CSV file'),
),
'#default_value' => isset($instance['widget']['settings']['data_sources']) ? $instance['widget']['settings']['data_sources'] : array(
'paste',
'upload',
),
);
return $form;
}
/**
* Implements hook_field_settings_form().
*/
function tablefield_field_settings_form($field, $instance, $has_data) {
$multiple_fields_remove_button_module = l(t('Multiple Fields Remove Button module'), 'https://www.drupal.org/project/multiple_fields_remove_button', array(
'attributes' => array(
'title' => t('Project page on Drupal.org'),
'target' => '_blank',
),
));
$manage_display = l(t('Manage display'), '/admin/structure/types/manage/' . $instance['bundle'] . '/display', array(
'attributes' => array(
'title' => t('More tablefield options'),
),
));
$form = array();
$form['message'] = array(
'#markup' => t('If multiple values are allowed it is recommended to install and enable the !multiple_fields_remove_button_module. Additional TableField settings are available at !manage_display.', array(
'!multiple_fields_remove_button_module' => $multiple_fields_remove_button_module,
'!manage_display' => $manage_display,
)),
'#weight' => -100,
);
return $form;
}
/**
* Form element validation handler for #type 'tablefield_number'.
*
* Note that #required is validated by _form_validate() already.
*/
function tablefield_validate_number($element, &$form_state) {
$value = $element['#value'];
if ($value === '') {
return;
}
$name = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
// Ensure the input is numeric.
if (!is_numeric($value)) {
form_error($element, t('%name must be a number.', array(
'%name' => $name,
)));
return;
}
// Ensure that the input is greater than the #min property, if set.
if (isset($element['#min']) && $value < $element['#min']) {
form_error($element, t('%name must be higher or equal to %min.', array(
'%name' => $name,
'%min' => $element['#min'],
)));
}
// Ensure that the input is less than the #max property, if set.
if (isset($element['#max']) && $value > $element['#max']) {
form_error($element, t('%name must be below or equal to %max.', array(
'%name' => $name,
'%max' => $element['#max'],
)));
}
if (isset($element['#step']) && strtolower($element['#step']) != 'any') {
// Check that the input is an allowed multiple of #step (offset by #min if
// #min is set).
$offset = isset($element['#min']) ? $element['#min'] : 0.0;
if (!tablefield_valid_number_step($value, $element['#step'], $offset)) {
form_error($element, t('%name is not a multiple of %step.', array(
'%name' => $name,
'%step' => $element['#step'],
)));
}
}
}
/**
* Verifies that a number is a multiple of a given step.
*
* The implementation assumes it is dealing with IEEE 754 double precision
* floating point numbers that are used by PHP on most systems.
*
* This is based on the number/range verification methods of webkit.
*
* @param float $value
* The value that needs to be checked.
* @param float $step
* The step scale factor. Must be positive.
* @param float $offset
* (optional) An offset, to which the difference must be a multiple of the
* given step.
*
* @return bool
* TRUE if no step mismatch has occured, or FALSE otherwise.
*
* @see http://opensource.apple.com/source/WebCore/WebCore-1298/html/NumberInputType.cpp
*/
function tablefield_valid_number_step($value, $step, $offset = 0.0) {
$double_value = (double) abs($value - $offset);
// The fractional part of a double has 53 bits. The greatest number that could
// be represented with that is 2^53. If the given value is even bigger than
// $step * 2^53, then dividing by $step will result in a very small remainder.
// Since that remainder can't even be represented with a single precision
// float the following computation of the remainder makes no sense and we can
// safely ignore it instead.
if ($double_value / pow(2.0, 53) > $step) {
return TRUE;
}
// Now compute that remainder of a division by $step.
$remainder = (double) abs($double_value - $step * round($double_value / $step));
// $remainder is a double precision floating point number. Remainders that
// can't be represented with single precision floats are acceptable. The
// fractional part of a float has 24 bits. That means remainders smaller than
// $step * 2^-24 are acceptable.
$computed_acceptable_error = (double) ($step / pow(2.0, 24));
return $computed_acceptable_error >= $remainder || $remainder >= $step - $computed_acceptable_error;
}
/**
* Implements hook_field_prepare_view().
*/
function tablefield_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
// Reset the array's internal pointer to the first element.
$instance = reset($instances);
foreach ($entities as $id => $entity) {
$entity_items =& $items[$id];
tablefield_field_presave($entity_type, $entity, $field, $instance, $langcode, $entity_items);
}
}
/**
* Implements hook_field_presave().
*/
function tablefield_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
foreach ($items as $delta => $table) {
if (empty($table['value'])) {
$tablefield = array();
if (!empty($table['tablefield'])) {
unset($tablefield['tablefield']);
// Sort by weight.
uasort($table['tablefield']['tabledata'], 'drupal_sort_weight');
// Put the data in the desired order before saving.
$row_counter = $col_counter = 0;
foreach ($table['tablefield']['tabledata'] as $row) {
foreach ($row as $key => $cell) {
if ($key === 'weight') {
$tablefield['tablefield']['tabledata']['row_' . $row_counter]['weight'] = $cell;
}
else {
$tablefield['tablefield']['tabledata']['row_' . $row_counter]['col_' . $col_counter] = $cell;
}
$col_counter++;
}
$row_counter++;
$col_counter = 0;
}
// Clear the old table data and repopulate it with the new values.
unset($table['tablefield']['tabledata']);
$table['tablefield']['tabledata'] = $tablefield['tablefield']['tabledata'];
// Add the non-value data back in before we save.
$tablefield = array_merge($tablefield, $table);
}
$items[$delta]['value'] = isset($tablefield['tablefield']) ? serialize($tablefield['tablefield']) : '';
}
elseif (empty($table['tablefield'])) {
// Batch processing only provides the 'value'.
$items[$delta]['tablefield'] = unserialize($items[$delta]['value']);
}
}
}
/**
* Implements hook_field_validate().
*/
function tablefield_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
// Catch empty form submissions for required tablefields.
if ($instance['required'] && isset($items[0]) && tablefield_field_is_empty($items[0], $field)) {
$message = t('@field is a required field.', array(
'@field' => $instance['label'],
));
$errors[$field['field_name']][$langcode][0]['tablefield'][] = array(
'error' => 'empty_required_tablefield',
'message' => $message,
);
}
}
/**
* Implements hook_field_widget_error().
*/
function tablefield_field_widget_error($element, $error, $form, &$form_state) {
form_error($element['tablefield'], $error[0]['message']);
}
/**
* Implements hook_field_load().
*/
function tablefield_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
if (isset($item['value'])) {
$items[$id][$delta]['tabledata'] = unserialize($item['value']);
}
}
}
}
/**
* Implements hook_field_is_empty().
*/
function tablefield_field_is_empty($item, $field) {
// @todo, is this the best way to mark the default value form?
// if we don't, it won't save the number of rows/cols
// Allow the system settings form to have an emtpy table
if (arg(0) == 'admin') {
return FALSE;
}
// Check for already serialized data. This is the case for other language
// versions of this field.
if (!empty($item['tabledata'])) {
return FALSE;
}
$count_rows = $item['tablefield']['rebuild']['count_rows'];
// Remove the preference fields to see if the table cells are all empty.
unset($item['tablefield']['caption']);
unset($item['tablefield']['rebuild']);
unset($item['tablefield']['import']);
unset($item['tablefield']['paste']);
if (!empty($item['tablefield']['tabledata'])) {
for ($i = 0; $i < $count_rows; $i++) {
foreach ($item['tablefield']['tabledata']["row_{$i}"] as $key => $cell) {
// Keys denoting weight data and anything still iterateable should be
// ignored while checking for empty cell data.
if (strpos($key, 'weight') === FALSE && !is_array($cell) && !empty($cell)) {
return FALSE;
}
}
}
}
return TRUE;
}
/**
* Implements hook_field_formatter_info().
*/
function tablefield_field_formatter_info() {
return array(
'tablefield_default' => array(
'label' => t('Tabular view'),
'field types' => array(
'tablefield',
),
'settings' => array(
'sticky_header' => TRUE,
'striping' => TRUE,
'sortable' => FALSE,
'hide_header' => FALSE,
'hide_empty_rows' => FALSE,
'hide_empty_cols' => FALSE,
'hide_cols_skip_head' => FALSE,
'trim_trailing_cols' => FALSE,
'trim_trailing_rows' => FALSE,
'table_custom_class_attributes' => 'table',
'export_csv' => FALSE,
),
),
'format_raw' => array(
'label' => t('Raw data (JSON or XML)'),
'field types' => array(
'tablefield',
),
'settings' => array(
'tabledatakey' => 'tabledata',
'usearraykeys' => 'No',
'rowkey' => FALSE,
'vertheader' => FALSE,
'tabledataonly' => TRUE,
'numeric_check' => TRUE,
'xml' => FALSE,
'xml_safe' => 'cdata',
),
),
);
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function tablefield_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = array();
switch ($display['type']) {
case 'format_raw':
$summary[] = t('Wrapper for table data (if applicable): %tr', array(
'%tr' => $settings['tabledatakey'],
));
$summary[] = t('Use first row/column values as keys (if not empty): %tr', array(
'%tr' => $settings['usearraykeys'],
));
if ($settings['usearraykeys'] === 'Both') {
$summary[] = t('Row identifier key: %tr', array(
'%tr' => t('Disabled'),
));
}
else {
$summary[] = t('Row identifier key: %tr', array(
'%tr' => $settings['rowkey'] ? t('Yes') : t('No'),
));
}
$summary[] = t('Vertical header (first column instead of first row): %tr', array(
'%tr' => $settings['vertheader'] ? t('Yes') : t('No'),
));
$summary[] = t('Table data only (no caption): %tr', array(
'%tr' => $settings['tabledataonly'] ? t('Yes') : t('No'),
));
if ($settings['xml']) {
$summary[] = t('Encode numeric strings as numbers: %tr', array(
'%tr' => t('Disabled'),
));
}
else {
$summary[] = t('Encode numeric strings as numbers: %tr', array(
'%tr' => $settings['numeric_check'] ? t('Yes') : t('No'),
));
}
$summary[] = t('XML instead of JSON: %tr', array(
'%tr' => $settings['xml'] ? t('Yes') : t('No'),
));
if (!$settings['xml']) {
$summary[] = t('How to make field values XML safe?: %tr', array(
'%tr' => t('Disabled'),
));
}
else {
$summary[] = t('How to make field values XML safe?: %tr', array(
'%tr' => $settings['xml_safe'],
));
}
break;
default:
$summary[] = t('Sticky header: %tr', array(
'%tr' => $settings['sticky_header'] ? t('Yes') : t('No'),
));
$summary[] = t('Striping (odd/even class): %tr', array(
'%tr' => $settings['striping'] ? t('Yes') : t('No'),
));
$summary[] = t('Sortable: %tr', array(
'%tr' => $settings['sortable'] ? t('Yes') : t('No'),
));
$summary[] = t('Hide first row: %tr', array(
'%tr' => $settings['hide_header'] ? t('Yes') : t('No'),
));
$summary[] = t('Hide empty columns ignoring column header: %tr', array(
'%tr' => $settings['hide_cols_skip_head'] ? t('Yes') : t('No'),
));
$summary[] = t('Trim empty trailing rows: %tr', array(
'%tr' => $settings['trim_trailing_rows'] ? t('Yes') : t('No'),
));
$summary[] = t('Trim empty trailing columns: %tr', array(
'%tr' => $settings['trim_trailing_cols'] ? t('Yes') : t('No'),
));
$summary[] = t('Hide empty rows: %tr', array(
'%tr' => $settings['hide_empty_rows'] ? t('Yes') : t('No'),
));
$summary[] = t('Hide empty columns: %tr', array(
'%tr' => $settings['hide_empty_cols'] ? t('Yes') : t('No'),
));
$permission = l(t('permission'), 'admin/people/permissions', array(
'fragment' => 'module-tablefield',
'attributes' => array(
'title' => t('Manage user permissions'),
),
));
$table_custom_class_attributes = isset($settings['table_custom_class_attributes']) ? $settings['table_custom_class_attributes'] : NULL;
$summary[] = t('Add custom class attribute classes to table html element: %tr', array(
'%tr' => $table_custom_class_attributes,
));
$summary[] = t('Show link to export table data as CSV depending on !permission: %tr', array(
'%tr' => $settings['export_csv'] ? t('Yes') : t('No'),
'!permission' => $permission,
));
}
return implode('<br />', $summary);
}
/**
* Implements hook_field_formatter_settings_form().
*/
function tablefield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
switch ($display['type']) {
case 'format_raw':
// Get a machine name from the field name (without 'field_' prefix).
$prefix = 'field_';
$machine = $instance['field_name'];
$select = 'select';
$select_or_other_module = l(t('Select (or other) module'), 'https://drupal.org/project/select_or_other', array(
'attributes' => array(
'title' => t('Select (or other) | Drupal.org'),
'target' => '_blank',
),
));
$description = t('To provide a custom value install and enable the !select_or_other_module.', array(
'!select_or_other_module' => $select_or_other_module,
));
if (module_exists('select_or_other')) {
$select = 'select_or_other';
$description = NULL;
}
if (substr($machine, 0, strlen($prefix)) == $prefix) {
$machine = substr($machine, strlen($prefix));
}
$element['tabledatakey'] = array(
'#type' => $select,
'#title' => t('Wrapper for table data (if applicable)'),
'#description' => $description,
'#options' => array(
'tabledata' => t('tabledata (fixed string)'),
$instance['label'] => t('Label: @label', array(
'@label' => $instance['label'],
)),
$machine => t('Machine name: @machine', array(
'@machine' => $machine,
)),
),
'#default_value' => $settings['tabledatakey'],
'#other_unknown_defaults' => 'other',
'#other_delimiter' => FALSE,
);
$element['usearraykeys'] = array(
'#type' => 'select',
'#title' => t('Use first row/column values as keys (if not empty)'),
'#options' => array(
'No' => t('No'),
'Header' => t('Header only'),
'Both' => t('Both first row and first column (two headers, horizontal and vertical)'),
),
'#default_value' => $settings['usearraykeys'],
);
$element['rowkey'] = array(
'#title' => t('Row identifier key'),
'#type' => 'checkbox',
'#default_value' => $settings['rowkey'],
);
if ($settings['usearraykeys'] === 'Both') {
$element['rowkey']['#disabled'] = TRUE;
$element['rowkey']['#title'] = '<span class="grayed-out">' . t('Row identifier key') . '</span> | ' . t("Disabled because above the first column values are set to be used as row identifiers.");
}
$element['vertheader'] = array(
'#title' => t('Vertical header (first column instead of first row)'),
'#type' => 'checkbox',
'#default_value' => $settings['vertheader'],
);
$element['tabledataonly'] = array(
'#title' => t('Table data only (no caption)'),
'#type' => 'checkbox',
'#default_value' => $settings['tabledataonly'],
);
$element['numeric_check'] = array(
'#title' => t('Encode numeric strings as numbers'),
'#type' => 'checkbox',
'#default_value' => $settings['numeric_check'],
);
if ($settings['xml']) {
$element['numeric_check']['#disabled'] = TRUE;
$element['numeric_check']['#title'] = '<span class="grayed-out">' . t('Encode numeric strings as numbers') . '</span> | ' . t("Disabled because below XML is selected that does not use quotes around any values.");
}
$element['xml'] = array(
'#title' => t('XML instead of JSON'),
'#type' => 'checkbox',
'#default_value' => $settings['xml'],
);
$element['xml_safe'] = array(
'#type' => 'select',
'#title' => t('How to make field values XML safe?'),
'#options' => array(
'htmlspecialchars' => t('Convert special characters to HTML entities (htmlspecialchars)'),
'cdata' => t('Represent field values that contain special characters as a CDATA section'),
'cdata_all' => t('Represent all field values as a CDATA section'),
),
'#default_value' => $settings['xml_safe'],
);
if (!$settings['xml']) {
$element['xml_safe']['#disabled'] = TRUE;
$element['xml_safe']['#title'] = '<span class="grayed-out">' . t('How to make field values XML safe?') . '</span> | ' . t("Disabled because above XML is not selected.");
}
break;
default:
$element['sticky_header'] = array(
'#title' => t('Sticky header'),
'#type' => 'checkbox',
'#default_value' => $settings['sticky_header'],
);
$element['striping'] = array(
'#title' => t('Striping (odd/even class)'),
'#type' => 'checkbox',
'#default_value' => $settings['striping'],
);
$element['sortable'] = array(
'#title' => t('Sortable'),
'#type' => 'checkbox',
'#default_value' => $settings['sortable'],
);
$tablesorter_module = l(t('Tablesorter module'), 'https://drupal.org/project/tablesorter', array(
'attributes' => array(
'title' => t('Tablesorter | Drupal.org'),
'target' => '_blank',
),
));
$element['hide_header'] = array(
'#title' => t('Hide first row'),
'#type' => 'checkbox',
'#default_value' => $settings['hide_header'],
);
$element['hide_cols_skip_head'] = array(
'#title' => t('Hide empty columns ignoring column header'),
'#description' => t('This will remove the table field completely if all columns are empty including the field label.'),
'#type' => 'checkbox',
'#default_value' => $settings['hide_cols_skip_head'],
);
$element['trim_trailing_cols'] = array(
'#title' => t('Trim empty trailing columns'),
'#type' => 'checkbox',
'#default_value' => $settings['trim_trailing_cols'],
);
$element['trim_trailing_rows'] = array(
'#title' => t('Trim empty trailing rows'),
'#type' => 'checkbox',
'#default_value' => $settings['trim_trailing_rows'],
);
$element['hide_empty_rows'] = array(
'#title' => t('Hide empty rows'),
'#type' => 'checkbox',
'#default_value' => $settings['hide_empty_rows'],
);
$element['hide_empty_cols'] = array(
'#title' => t('Hide empty columns'),
'#type' => 'checkbox',
'#default_value' => $settings['hide_empty_cols'],
);
$permission = l(t('permission'), 'admin/people/permissions', array(
'fragment' => 'module-tablefield',
'attributes' => array(
'title' => t('Manage user permissions'),
),
));
$element['table_custom_class_attributes'] = array(
'#title' => t('Add custom class attribute classes to the table html element'),
'#type' => 'textfield',
'#size' => 40,
'#default_value' => $settings['table_custom_class_attributes'],
);
$element['export_csv'] = array(
'#title' => t('Show link to export table data as CSV depending on !permission', array(
'!permission' => $permission,
)),
'#type' => 'checkbox',
'#default_value' => $settings['export_csv'],
);
}
return $element;
}
/**
* Implements hook_field_formatter_view().
*/
function tablefield_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
$settings = $display['settings'];
$formatter = $display['type'];
if (!empty($items)) {
switch ($display['type']) {
case 'format_raw':
$values = array();
foreach ($items as $delta => $table) {
$value = unserialize($table['value']);
// Swap rows and columns if the header is vertical (first column).
if ($settings['vertheader']) {
$transposed = tablefield_transpose($value['tabledata']);
unset($value['tabledata']);
array_pop($transposed);
// Swap column and row key names.
foreach ($transposed as $colk => $colv) {
foreach ($colv as $rowk => $rowv) {
$value['tabledata'][str_replace('col_', 'row_', $colk)][str_replace('row_', 'col_', $rowk)] = $rowv;
}
// Add a 'weight' although the value is not relevant.
$value['tabledata'][str_replace('col_', 'row_', $colk)]['weight'] = 0;
}
}
// Remove unneeded data.
unset($value['rebuild']);
unset($value['import']);
unset($value['paste']);
// DEVELOPERS! Extra future data should be captured and unset below.
$caption = $value['caption'];
unset($value['caption']);
if ($settings['usearraykeys'] === 'Header' || $settings['usearraykeys'] === 'Both') {
// Keep first row values (header) to use for array keys later.
$keys = $value['tabledata']['row_0'];
// Remove linebreaks from array keys.
array_walk_recursive($keys, function (&$key) {
$key = str_replace(array(
"\r",
"\n",
"<br />",
), '', $key);
});
// If a header value is empty use the column key (col_#).
foreach ($keys as $key => $content) {
if (empty($content)) {
$keys[$key] = $key;
}
}
// Warning about columns with duplicate names being suppressed.
$unique = array_unique($keys);
$duplicates = array_diff_assoc($keys, $unique);
foreach ($duplicates as $duplicate) {
drupal_set_message(t('The column header "%key" appears multiple times in a table. In the JSON output only the last column with this key is used to avoid duplicate names.', array(
'%key' => $duplicate,
)), 'warning', FALSE);
}
unset($keys['weight']);
// Remove the first row (header).
unset($value['tabledata']['row_0']);
if ($settings['usearraykeys'] === 'Both') {
// Remove the first column from the keys.
unset($keys['col_0']);
// Make number of elements same as $row for array_combine later.
unset($keys['weight']);
}
}
foreach ($value['tabledata'] as $key => $row) {
unset($row['weight']);
if ($settings['usearraykeys'] === 'Header' || $settings['usearraykeys'] === 'Both') {
if ($settings['usearraykeys'] === 'Both') {
$row_ident = $row['col_0'];
// Clean up unneeded data.
unset($value['tabledata'][$key]);
unset($row['col_0']);
$row = array_combine($keys, $row);
// If the first column value is empty use the row key (row_#).
$row_id = empty($row_ident) ? $key : $row_ident;
$row_id = str_replace(array(
"\r",
"\n",
"<br />",
), '', $row_id);
// Warning about rows with duplicate names being suppressed.
if (isset($value['tabledata'][$row_id])) {
drupal_set_message(t('The row header "%row_id" appears multiple times in a table. In the JSON output only the last row with this key is used to avoid duplicate names.', array(
'%row_id' => $row_id,
)), 'warning', FALSE);
}
$value['tabledata'][$row_id] = $row;
// Remove the old key from the data set if it was replaced.
if (!empty($row_ident)) {
unset($value['tabledata'][$key]);
}
}
$unique = array_unique($keys);
$row = array_combine($unique, $row);
}
if ($settings['usearraykeys'] != 'Both') {
$value['tabledata'][$key] = $row;
}
}
// For a row identifier key to be retained we need to insert a
// numeric index key for each row.
if ($settings['usearraykeys'] === 'Both' || $settings['rowkey']) {
$i = 0;
foreach ($value as $key => $val) {
$value[$i][$key] = $val;
// Remove original key. We'll get it back with array_values().
unset($value[$key]);
$i++;
}
}
if ($settings['tabledataonly']) {
$value = reset($value);
}
else {
// DEVELOPERS! Extra future data should be unset below (e.g. title).
$value['caption'] = $caption;
}
// Use the set wrapper for the table data.
if (isset($value[0]['tabledata']) && $settings['tabledatakey'] !== 'tabledata') {
$value[0][$settings['tabledatakey']] = $value[0]['tabledata'];
unset($value[0]['tabledata']);
}
elseif (isset($value['tabledata']) && $settings['tabledatakey'] !== 'tabledata') {
$value[$settings['tabledatakey']] = $value['tabledata'];
unset($value['tabledata']);
}
// Add the table to the final output.
if ($settings['tabledataonly']) {
array_push($values, array_values($value));
}
else {
$value[$settings['tabledatakey']] = $settings['usearraykeys'] === 'Both' || $settings['rowkey'] ? $value[0][$settings['tabledatakey']] : array_values($value[$settings['tabledatakey']]);
unset($value[0]);
krsort($value);
array_push($values, $value);
}
// If the field is not plain text and a text area then remove
// linebreaks.
$format = isset($items[$delta]['format']) ? $items[$delta]['format'] : 'plain_text';
array_walk_recursive($values, function (&$value) use ($format, $settings) {
if ($format !== 'plain_text') {
if ($settings['xml']) {
switch ($settings['xml_safe']) {
case 'htmlspecialchars':
$value = str_replace(array(
"\r",
"\n",
), '', htmlspecialchars($value));
break;
case 'cdata':
if (preg_match('/[&"\'<>]/', $value)) {
$value = str_replace(array(
"\r",
"\n",
), '', '<![CDATA[' . str_replace(']]>', '', $value) . ']]>');
}
else {
$value = str_replace(array(
"\r",
"\n",
), '', $value);
}
break;
default:
$value = str_replace(array(
"\r",
"\n",
), '', '<![CDATA[' . str_replace(']]>', '', $value) . ']]>');
break;
}
}
else {
$value = str_replace(array(
"\r",
"\n",
), '', $value);
}
}
});
}
// Serve pretty print with a fixed-width font and preserve spaces and
// line breaks.
// Show pretty print on node pages and raw on other pages.
$prettyprint = menu_get_object() && arg(2) !== 'themeless' ? JSON_PRETTY_PRINT : FALSE;
if ($prettyprint) {
$element['#prefix'] = '<pre>';
$element['#suffix'] = '</pre>';
}
$numeric_check = $settings['numeric_check'] ? JSON_NUMERIC_CHECK : FALSE;
// Avoid a wrapping array if we have a single table (not multi-value).
if (!(count($values) - 1)) {
$values = array_shift($values);
}
// Render multiple values as one to ensure valid JSON (e.g. seperator)
// or XML.
if ($settings['xml']) {
// Creating object of SimpleXMLElement.
$xml_data = new SimpleXMLElement('<?xml version="1.0"?><' . $settings['tabledatakey'] . '></' . $settings['tabledatakey'] . '>');
// Flatten the array if we only have one element.
if (count($values) === 1) {
$values = current($values);
}
// Function call to convert array to xml.
array_to_xml($values, $xml_data);
if ($prettyprint) {
$dom = dom_import_simplexml($xml_data)->ownerDocument;
$dom->formatOutput = TRUE;
$output = $dom
->saveXML();
$markup = '<xmp>' . $output . '</xmp>';
// Let other modules change the output.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'field' => $field,
'instance' => $instance,
'language' => $langcode,
'items' => $items,
'display' => $display,
'table_field_output_type' => 'xml_pretty_print',
);
drupal_alter('tablefield_output', $markup, $context);
$element[0] = array(
'#markup' => $markup,
);
}
else {
$output = $xml_data
->asXML();
$markup = str_replace('<?xml version="1.0"?>', '', $output);
// Let other modules change the output.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'field' => $field,
'instance' => $instance,
'language' => $langcode,
'items' => $items,
'display' => $display,
'table_field_output_type' => 'xml_raw',
);
drupal_alter('tablefield_output', $markup, $context);
$element[0] = array(
'#markup' => $markup,
);
}
}
else {
$markup = json_encode($values, $prettyprint | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | $numeric_check);
// Let other modules change the output.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'field' => $field,
'instance' => $instance,
'language' => $langcode,
'items' => $items,
'display' => $display,
'table_field_output_type' => $prettyprint ? 'json_pretty_print' : 'json_raw',
);
drupal_alter('tablefield_output', $markup, $context);
$element[0] = array(
// See http://php.net/manual/en/json.constants.php#119565.
'#markup' => check_plain($markup),
);
}
break;
default:
foreach ($items as $delta => $table) {
// Check for table caption.
$raw = unserialize($table['value']);
$caption = isset($raw['caption']) ? check_plain($raw['caption']) : '';
// Rationalize the stored data.
if (!empty($table['tablefield'])) {
$tabledata = $table['tablefield'];
}
elseif (!empty($table['value'])) {
$tabledata = unserialize($table['value']);
}
// Run the table through input filters.
if (isset($tabledata['tabledata'])) {
if (!empty($tabledata['tabledata'])) {
if ($settings['trim_trailing_rows']) {
$tabledata['tabledata'] = tablefield_trim($tabledata['tabledata']);
}
if ($settings['trim_trailing_cols']) {
$tabledata['tabledata'] = tablefield_rtrim_cols($tabledata['tabledata']);
}
if ($settings['hide_empty_rows']) {
$tabledata['tabledata'] = tablefield_hide_rows($tabledata['tabledata']);
}
if ($settings['hide_empty_cols']) {
$tabledata['tabledata'] = tablefield_hide_cols($tabledata['tabledata']);
}
if ($settings['hide_cols_skip_head']) {
$tabledata['tabledata'] = tablefield_hide_cols($tabledata['tabledata'], TRUE);
}
foreach ($tabledata['tabledata'] as $row_key => $row) {
foreach ($row as $col_key => $cell) {
if (!empty($table['format']) && $col_key !== 'weight') {
$tabledata[$row_key][$col_key] = array(
'data' => check_markup($cell, $table['format']),
'class' => array(
$row_key,
$col_key,
),
);
}
elseif ($col_key !== 'weight') {
$tabledata[$row_key][$col_key] = array(
'data' => check_plain($cell),
'class' => array(
$row_key,
$col_key,
),
);
}
}
}
}
// Grab the header orientation.
$header_orientation = !empty($tabledata['rebuild']['header_orientation']) ? $tabledata['rebuild']['header_orientation'] : (isset($instance['default_value'][0]['tablefield']['rebuild']['header_orientation']) ? $instance['default_value'][0]['tablefield']['rebuild']['header_orientation'] : 'Horizontal');
// Pull the header for theming.
unset($tabledata['caption']);
unset($tabledata['tabledata']);
unset($tabledata['rebuild']);
unset($tabledata['import']);
unset($tabledata['paste']);
$header_data = isset($tabledata['row_0']) ? $tabledata['row_0'] : NULL;
// Check for an empty header, if so we don't want to theme it.
$noheader = TRUE;
if (empty($settings['hide_header']) && $header_data) {
foreach ($header_data as $cell) {
if (strlen($cell['data']) > 0) {
$noheader = FALSE;
break;
}
}
}
$header = $noheader ? [] : $header_data;
$entity_info = entity_get_info($entity_type);
$entity_id = !empty($entity_info['entity keys']['id']) ? $entity->{$entity_info['entity keys']['id']} : NULL;
// Remove the first row from the tabledata.
if (empty($settings['hide_header']) && $header_data || $settings['hide_header']) {
array_shift($tabledata);
}
// Theme the table for display, but only if we have printable
// table data.
if (!$settings['hide_cols_skip_head'] || $tabledata || $header) {
$element[$delta] = array(
'#theme' => 'tablefield_view',
'#caption' => $caption,
'#header_orientation' => $header_orientation,
'#sticky' => isset($settings['sticky_header']) ? $settings['sticky_header'] : NULL,
'#striping' => isset($settings['striping']) ? $settings['striping'] : NULL,
'#sortable' => isset($settings['sortable']) ? $settings['sortable'] : NULL,
'#header' => $header,
'#rows' => $tabledata,
'#delta' => $delta,
'#table_custom_class_attributes' => !trim($settings['table_custom_class_attributes']) == FALSE ? $settings['table_custom_class_attributes'] : NULL,
'#export' => isset($settings['export_csv']) ? $settings['export_csv'] : NULL,
'#entity_type' => $entity_type,
'#entity_id' => $entity_id,
'#field_name' => $field['field_name'],
'#langcode' => $langcode,
'#formatter' => $formatter,
);
}
}
}
}
}
return $element;
}
/**
* Implements hook_field_widget_info().
*/
function tablefield_field_widget_info() {
return array(
'tablefield' => array(
'label' => t('Table field'),
'field types' => array(
'tablefield',
),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
),
);
}
/**
* Implements hook_field_widget_form().
*/
function tablefield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$form['#after_build'][] = 'tablefield_after_build';
$settings = isset($instance['display']['default']['settings']) ? $instance['display']['default']['settings'] : FALSE;
$element['#type'] = 'tablefield';
$form['#attributes']['enctype'] = 'multipart/form-data';
if ($settings) {
$hide_header = isset($settings['hide_header']) ? $settings['hide_header'] : FALSE;
}
/* Tablefield is sometimes embedded within another form by other modules, such
* as Field Collection. Because of that, we cannot rely on field_name and
* $delta to provide a unique ID for this element. Instead we use a
* concatenation of the field parents along with the current field name,
* language, delta and tablefield key.
*/
$tablefield_parents = isset($element['#field_parents']) ? $element['#field_parents'] : array();
array_push($tablefield_parents, $element['#field_name'], $element['#language'], $element['#delta'], 'tablefield');
$id = drupal_clean_css_identifier(implode('-', $tablefield_parents));
// IDs to use for various buttons/wrapper for this element. When processing
// an AJAX request, these IDs are used to build the field stack so we know
// where the value we're adjusting is in the FormAPI array.
$ajax_wrapper_id = "{$id}-wrapper";
$rebuild_id = "{$id}-rebuild";
$import_id = "{$id}-import";
$pasted_id = "{$id}-pasted";
// Default table size.
$default_size['count_cols'] = isset($instance['default_value'][0]['tablefield']['rebuild']['count_cols']) ? $instance['default_value'][0]['tablefield']['rebuild']['count_cols'] : 5;
$default_size['count_rows'] = isset($instance['default_value'][0]['tablefield']['rebuild']['count_rows']) ? $instance['default_value'][0]['tablefield']['rebuild']['count_rows'] : 5;
$default_size['header_orientation'] = isset($instance['default_value'][0]['tablefield']['rebuild']['header_orientation']) ? $instance['default_value'][0]['tablefield']['rebuild']['header_orientation'] : 'Horizontal';
// Default table cell values.
$default_value = NULL;
// If there's a triggering element get the values from form state.
if (isset($form_state['triggering_element'])) {
if ($form_state['triggering_element']['#name'] == $rebuild_id) {
// Rebuilding table rows/cols.
$default_value = drupal_array_get_nested_value($form_state['tablefield_rebuild'], $tablefield_parents);
drupal_set_message(t('Table structure rebuilt.'), 'status', FALSE);
}
elseif ($form_state['triggering_element']['#name'] == $import_id) {
// Importing CSV data.
tablefield_import_csv($form, $form_state, $langcode, $import_id, $tablefield_parents);
$default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
$identifier = implode('--', $tablefield_parents);
$form_state['_tablefield_imported_values'][$identifier] = $default_value;
}
elseif ($form_state['triggering_element']['#name'] == $pasted_id) {
// Importing pasted data.
tablefield_import_pasted($form, $form_state, $langcode, $pasted_id, $tablefield_parents);
$default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
if (empty($default_value['rebuild'])) {
$default_value['rebuild'] = $default_size;
}
}
else {
// The triggering element is neither a rebuild nor an import
// e.g. a file upload.
$default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
}
}
// If no values by now, get tablefield item value stored in database, if any.
if (!$default_value) {
// This could be set e.g. when using paragraphs module.
if (isset($items[$delta]['tablefield'])) {
$default_value = $items[$delta]['tablefield'];
}
elseif (isset($items[$delta]['value'])) {
$default_value = unserialize($items[$delta]['value']);
}
elseif ($delta === 0 && isset($instance['default_value'][0]['tablefield'])) {
$default_value = $instance['default_value'][0]['tablefield'];
}
}
if (empty($default_value['rebuild'])) {
$default_value['rebuild'] = $default_size;
}
else {
// If this is data that was saved before 'header_orientation' was a
// per-value setting, then we need to keep the valuef from $default_size.
if (empty($default_value['rebuild']['header_orientation'])) {
$default_value['rebuild']['header_orientation'] = $default_size['header_orientation'];
}
$default_size = $default_value['rebuild'];
}
$count_rows = $default_size['count_rows'];
$count_cols = $default_size['count_cols'];
$header_orientation = $default_size['header_orientation'];
// Now we can build the widget.
$help_text = '';
if (!empty($instance['description'])) {
$help_text = $instance['description'];
}
else {
if ($settings) {
$display_settings = l(t('default display settings'), 'admin/structure/types/manage/' . $element['#bundle'] . '/display', array(
'attributes' => array(
'title' => t('Manage display settings'),
),
));
if ($hide_header == TRUE) {
$help_text = t('This table will not have a header according to the !display_settings.', array(
'!display_settings' => $display_settings,
));
}
else {
$help_text = t('The first row will appear as the table header. Leave the first row blank if you do not need a header.');
}
}
}
$element['tablefield'] = array(
'#title' => $element['#title'],
'#description' => filter_xss_admin($help_text),
'#attributes' => array(
'id' => $id,
'class' => array(
'form-tablefield',
),
),
'#type' => 'fieldset',
'#tree' => TRUE,
'#collapsible' => FALSE,
'#prefix' => '<div id="' . $ajax_wrapper_id . '">',
'#suffix' => '</div>',
);
if ($settings) {
if ($hide_header) {
$element['tablefield']['#attributes']['class'][] = 'table-no-headers';
}
}
if (in_array($header_orientation, array(
'Horizontal',
'Both',
))) {
$element['tablefield']['#attributes']['class'][] = 'tablefield-header-orientation-horizontal';
}
if (in_array($header_orientation, array(
'Vertical',
'Both',
))) {
$element['tablefield']['#attributes']['class'][] = 'tablefield-header-orientation-vertical';
}
// Give the fieldset the appropriate class if it is required.
if ($element['#required']) {
$element['tablefield']['#title'] .= ' <span class="form-required" title="';
$element['tablefield']['#title'] .= t('This field is required');
$element['tablefield']['#title'] .= '">*</span>';
}
if (arg(0) == 'admin') {
$element['tablefield']['#description'] = t('This form defines the table field defaults, but the number of rows/columns and content can be overridden.');
if ($settings) {
if (!$hide_header) {
$element['tablefield']['#description'] .= ' ' . t('The first row will appear as the table header. Leave the first row blank if you do not need a header.');
}
}
}
// Render the form table.
$element['tablefield']['tabledata']['a_break'] = array(
'#markup' => '<table id="tablefield-editor">',
);
$default_value = isset($default_value['tabledata']) ? $default_value['tabledata'] : $default_value;
// Make added default values visible when editing existing content.
if (isset($instance['widget']['settings']['restrict_rebuild']) && $instance['widget']['settings']['restrict_rebuild'] && $instance['widget']['settings']['lock_values'] && arg(0) !== 'admin' && arg(0) !== 'system') {
// Use the default number of cols/rows if they changed.
$count_cols = $count_cols != $instance['default_value'][0]['tablefield']['rebuild']['count_cols'] ? $instance['default_value'][0]['tablefield']['rebuild']['count_cols'] : $count_cols;
$count_rows = $count_rows != $instance['default_value'][0]['tablefield']['rebuild']['count_rows'] ? $instance['default_value'][0]['tablefield']['rebuild']['count_rows'] : $count_rows;
}
// Loop over all the rows.
for ($i = 0; $i < $count_rows; $i++) {
$zebra = $i % 2 == 0 ? 'even' : 'odd';
// Disable table drag functionality in case of locked default cells under
// the first row or completely on the field settings form.
$draggable = 'draggable ';
unset($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]['weight']);
if (arg(0) == 'admin' || isset($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]) && array_filter($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"])) {
$draggable = FALSE;
}
$element['tablefield']['tabledata']['b_break' . $i] = array(
'#markup' => '<tr class="' . $draggable . 'tablefield-row-' . $i . ' ' . $zebra . '"><td class="tablefield-row-count">' . ($i + 1) . '</td>',
);
// Loop over all the columns.
for ($ii = 0; $ii < $count_cols; $ii++) {
$instance_default = isset($instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]["col_{$ii}"]) ? $instance['default_value'][0]['tablefield']['tabledata']["row_{$i}"]["col_{$ii}"] : array();
if (!empty($instance_default) && !empty($instance['widget']['settings']['lock_values']) && arg(0) != 'admin') {
// The value still needs to be send on every load in order for the
// table to be saved correctly.
$element['tablefield']['tabledata']['row_' . $i]['col_' . $ii] = array(
'#type' => 'value',
'#value' => $instance_default,
);
// Display the default value, since it's not editable.
$element['tablefield']['tabledata']['row_' . $i]['col_' . $ii . '_display'] = array(
'#type' => 'item',
'#title' => $instance_default,
'#prefix' => '<td class="col-' . $ii . '">',
'#suffix' => '</td>',
);
}
else {
$input_type = isset($instance['widget']['settings']['input_type']) ? $instance['widget']['settings']['input_type'] : 'textfield';
$max_length = isset($instance['widget']['settings']['max_length']) ? abs($instance['widget']['settings']['max_length']) : '2048';
$cell_default = isset($default_value['row_' . $i]['col_' . $ii]) ? $default_value['row_' . $i]['col_' . $ii] : '';
// If the field does not contain HTML, is not plain text and a text area
// then covert linebreaks to <br />.
$format = isset($items[$delta]['format']) ? $items[$delta]['format'] : 'plain_text';
if (!($cell_default != strip_tags($cell_default)) && $format !== 'plain_text') {
$nl2br = array(
'_cell_validate',
);
}
else {
$nl2br = NULL;
}
$element['tablefield']['tabledata']['row_' . $i]['col_' . $ii] = array(
'#type' => $input_type,
'#maxlength' => $max_length,
'#size' => 0,
'#attributes' => array(
'id' => $id . '-cell-' . $i . '-' . $ii,
'class' => array(
'tablefield-row-' . $i,
'tablefield-col-' . $ii,
),
'style' => 'min-width: 100%',
),
'#default_value' => $cell_default,
'#prefix' => '<td class="col-' . $ii . '">',
'#suffix' => '</td>',
'#element_validate' => $nl2br,
);
}
}
// Add an extra column for the weight.
$row_weight_default = isset($default_value[$i]['weight']) ? $default_value[$i]['weight'] : $i + 1;
$element['tablefield']['tabledata']['row_' . $i]['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
'#default_value' => $row_weight_default,
'#delta' => $count_rows,
'#attributes' => array(
'class' => array(
'tablefield-weight',
),
),
'#prefix' => '<td class="tabledrag-hide">',
'#suffix' => '</td>',
);
$element['tablefield']['c_break' . $i] = array(
'#markup' => '</tr>',
);
}
$element['tablefield']['t_break' . $i] = array(
'#markup' => '</table>',
);
// Provide caption field to describe the data contained in the table.
$element['tablefield']['caption'] = array(
'#type' => 'textfield',
'#title' => t('Table description'),
'#default_value' => '',
'#description' => t('This brief caption will be associated with the table and will help Assistive Technology, like screen readers, better describe the content within.'),
);
// Could be the Paragraphs module.
if (isset($items[$delta]['tablefield'])) {
$raw = $items[$delta]['tablefield'];
}
elseif (isset($items[$delta]['value'])) {
$raw = unserialize($items[$delta]['value']);
}
elseif ($delta === 0 && isset($instance['default_value'][0]['tablefield'])) {
$raw = $instance['default_value'][0]['tablefield'];
}
if (isset($raw['caption'])) {
$element['tablefield']['caption']['#default_value'] = check_plain($raw['caption']);
}
// If the user doesn't have rebuild perms, we pass along the data as a value.
// Otherwise we provide form elements to specify the size and ajax rebuild.
if (isset($instance['widget']['settings']['restrict_rebuild']) && $instance['widget']['settings']['restrict_rebuild'] && !user_access('rebuild tablefield') || isset($instance['widget']['settings']['restrict_rebuild']) && $instance['widget']['settings']['restrict_rebuild'] && $instance['widget']['settings']['lock_values'] && arg(0) !== 'admin' && arg(0) !== 'system') {
$element['tablefield']['rebuild'] = array(
'#type' => 'value',
'#tree' => TRUE,
'count_cols' => array(
'#type' => 'value',
'#value' => $count_cols,
),
'count_rows' => array(
'#type' => 'value',
'#value' => $count_rows,
),
'header_orientation' => array(
'#type' => 'value',
'#value' => $header_orientation,
),
'rebuild' => array(
'#type' => 'value',
'#value' => t('Rebuild Table'),
),
);
}
else {
$element['tablefield']['rebuild'] = array(
'#type' => 'fieldset',
'#tree' => TRUE,
'#title' => t('Number of rows/columns and header orientation'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$element['tablefield']['rebuild']['count_cols'] = array(
'#title' => t('How many Columns'),
'#type' => 'textfield',
'#size' => 5,
'#prefix' => '<div class="clearfix">',
'#suffix' => '</div>',
'#value' => $count_cols,
);
$element['tablefield']['rebuild']['count_rows'] = array(
'#title' => t('How many Rows'),
'#type' => 'textfield',
'#size' => 5,
'#prefix' => '<div class="clearfix">',
'#suffix' => '</div>',
'#value' => $count_rows,
);
$element['tablefield']['rebuild']['header_orientation'] = array(
'#type' => 'select',
'#title' => t('Header orientation'),
'#options' => array(
'Horizontal' => t('First row (horizontal)'),
'Vertical' => t('First column (vertical)'),
'Both' => t('Both first row and first column (two headers, horizontal and vertical)'),
),
'#default_value' => $header_orientation,
);
$element['tablefield']['rebuild']['rebuild'] = array(
'#type' => 'button',
'#validate' => array(),
'#limit_validation_errors' => array(),
'#executes_submit_callback' => TRUE,
'#submit' => array(
'tablefield_rebuild_form',
),
'#value' => t('Rebuild Table'),
'#name' => $rebuild_id,
'#attributes' => array(
'class' => array(
'tablefield-rebuild',
),
),
'#ajax' => array(
'callback' => 'tablefield_rebuild_form_ajax',
'wrapper' => $ajax_wrapper_id,
'effect' => 'fade',
),
);
}
if (user_access('always use additional datasources') || isset($instance['widget']['settings']['data_sources']['upload']) && $instance['widget']['settings']['data_sources']['upload'] === 'upload') {
// Allow the user to import a csv file.
$element['tablefield']['import'] = array(
'#type' => 'fieldset',
'#tree' => TRUE,
'#title' => t('Upload CSV file'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$element['tablefield']['import']['file'] = array(
'#name' => 'files[' . $import_id . ']',
'#title' => t('File upload'),
'#type' => 'file',
);
$element['tablefield']['import']['import'] = array(
'#type' => 'button',
'#validate' => array(),
'#limit_validation_errors' => array(),
'#executes_submit_callback' => TRUE,
'#submit' => array(
'tablefield_rebuild_form',
),
'#value' => t('Upload CSV'),
'#name' => $import_id,
'#attributes' => array(
'class' => array(
'tablefield-rebuild',
),
),
'#ajax' => array(
'callback' => 'tablefield_rebuild_form_ajax',
'wrapper' => $ajax_wrapper_id,
'effect' => 'fade',
),
);
}
if (user_access('always use additional datasources') || isset($instance['widget']['settings']['data_sources']['paste']) && $instance['widget']['settings']['data_sources']['paste'] === 'paste') {
// Allow user to paste data (e.g. from Excel).
$element['tablefield']['paste'] = array(
'#type' => 'fieldset',
'#tree' => TRUE,
'#title' => t('Copy & Paste'),
'#attributes' => array(
'class' => array(
'tablefield-extra tablefield-paste',
),
),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$delimiters = array(
'TAB' => t('TAB'),
',' => t('Comma ,'),
';' => t('Semicolon ;'),
'|' => t('Pipe |'),
'+' => t('Plus +'),
':' => t('Colon :'),
);
$element['tablefield']['paste']['paste_delimiter'] = array(
'#type' => 'select',
'#tree' => TRUE,
'#title' => t('Column separator'),
'#name' => 'delimiter[' . $pasted_id . ']',
'#options' => $delimiters,
'#description' => t('Data copied from Excel will use TAB.'),
);
$element['tablefield']['paste']['data'] = array(
'#type' => 'textarea',
'#tree' => TRUE,
'#name' => 'data[' . $pasted_id . ']',
'#title' => t('Paste table data here:'),
);
$element['tablefield']['paste']['paste_import'] = array(
'#type' => 'button',
'#validate' => array(),
'#limit_validation_errors' => array(),
'#executes_submit_callback' => TRUE,
'#submit' => array(
'tablefield_rebuild_form',
),
'#value' => t('Import & Rebuild'),
'#name' => $pasted_id,
'#ajax' => array(
'callback' => 'tablefield_rebuild_form_ajax',
'wrapper' => $ajax_wrapper_id,
'effect' => 'fade',
),
);
}
// Allow the user to select input filters.
if (!empty($instance['widget']['settings']['cell_processing'])) {
$element['#base_type'] = $element['#type'];
$element['#type'] = 'text_format';
$element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL;
if (module_exists('wysiwyg')) {
$element['#wysiwyg'] = FALSE;
}
}
$form['element']['#element_validate'][] = 'tablefield_field_widget_form_validate';
return $element;
}
/**
* Form validation callback.
*
* If the POST request exceeds the server's limit of max_input_vars, the
* tablefield values that were computed by a CSV import will be directly get set
* by the CSV table, bypassing the form submission step, and also any changes
* that the user made on browser.
*/
function tablefield_field_widget_form_validate(&$form, &$form_state, &$element) {
if (tablefield_post_exceeds_max_input_vars()) {
// Check if the values have been imported from a CSV file. If so, original
// values still exist in $form_state['_tablefield_imported_values'].
$values_from_csv = FALSE;
if (isset($form_state['_tablefield_imported_values'])) {
$values_from_csv = $form_state['_tablefield_imported_values'];
}
if ($values_from_csv) {
// Each key is one of the fields multiple values.
$elements = array_keys($values_from_csv);
foreach ($elements as $key) {
$table_values = $values_from_csv[$key];
// Reconstruct in the same form as the array is passed in $form_state.
$table_values_format = array();
$i_row = 0;
foreach ($table_values as $row) {
$i_col = 0;
foreach ($row as $cell) {
$table_values_format['cell_' . $i_row . '_' . $i_col] = $cell;
$i_col++;
}
$i_row++;
}
$keys = explode('--', $key);
tablefield_replace_csv_values($form_state['values'], $keys, $table_values_format);
$message_warning = t('This table is too large to be safely edited. The original CSV values are stored. Please import a new CSV file to make changes to this table.');
drupal_set_message($message_warning, 'warning');
}
}
else {
$message_error = t('This table is too large to be safely edited. To assure that no data is lost, please import a new CSV file to make changes to this table.');
drupal_set_message($message_error, 'error');
}
}
}
/**
* Replace table field values with the values that were saved.
*
* Helper function that uses recursion to replace table field values from
* $form_state['values'] with the values that were saved right after the csv
* upload.
*
* The function is used if the size of the table is so large, that on the form
* submission the max_input_vars is exceeded.
*
* @param array $destination
* It should only contain the $form_state['values'].
* @param array $keys
* Array with the nested keys of the field's location into
* $form_state['values'].
* @param array $value
* The table to be placed.
*/
function tablefield_replace_csv_values(array &$destination, array &$keys, array $value) {
$cur_key = array_shift($keys);
if (!isset($destination[$cur_key])) {
return;
}
if (!empty($keys)) {
tablefield_replace_csv_values($destination[$cur_key], $keys, $value);
return;
}
foreach ($value as $key => $value2) {
$destination[$cur_key][$key] = $value2;
}
}
/**
* Implements hook_custom_theme().
*/
function tablefield_custom_theme() {
// Ensure that if this is a valid POST request that we use the same theme
// used by the referring form.
if (isset($_POST['form_build_id']) && path_is_admin($_GET['q']) && user_access('view the administration theme')) {
return variable_get('admin_theme');
}
}
/**
* Custom callback for a textarea to be processed for linebreaks.
*/
function _cell_validate($element, &$form_state) {
$value = $element['#value'];
if (!empty($element['#value'])) {
$value = nl2br($value);
form_set_value($element, $value, $form_state);
}
}
/**
* Form #after_build callback for tablefield_field_widget_form().
*/
function tablefield_after_build($form, &$form_state) {
drupal_add_css(drupal_get_path('module', 'tablefield') . '/css/tablefield.css');
drupal_add_tabledrag('tablefield-editor', 'order', 'sibling', 'tablefield-weight');
return $form;
}
/**
* Helper function to import data from a CSV file.
*
* @param array $form
* The form structure.
* @param array $form_state
* The current state of the form.
* @param string $langcode
* The language associated with the form items.
* @param string $file_form_field_name
* The key of the upload form element in the form array.
* @param array $tablefield_parents
* The parents of the tablefield element.
*/
function tablefield_import_csv($form, &$form_state, $langcode, $file_form_field_name, $tablefield_parents) {
// Extract the field and file name from the id of the clicked button.
$file = file_save_upload($file_form_field_name, array(
'file_validate_extensions' => array(
'csv',
),
));
if (is_object($file)) {
// Assure Mac/Linux EOL markers work with fgetcsv().
$auto_detect_line_endings = ini_get('auto_detect_line_endings');
ini_set('auto_detect_line_endings', TRUE);
if (($handle = fopen($file->uri, "r")) !== FALSE) {
tablefield_delete_table_values(drupal_array_get_nested_value($form_state['values'], $tablefield_parents));
tablefield_delete_table_values(drupal_array_get_nested_value($form_state['input'], $tablefield_parents));
// Checking the encoding of the CSV file to be UTF-8.
$encoding = 'UTF-8';
if (function_exists('mb_detect_encoding')) {
$file_contents = file_get_contents($file->uri);
$encodings_list = implode(',', variable_get('tablefield_detect_encodings', array(
'UTF-8',
'ISO-8859-1',
'WINDOWS-1251',
)));
$encoding = mb_detect_encoding($file_contents, $encodings_list);
}
// Populate CSV values.
$max_col_count = 0;
$row_count = 0;
$imported_tablefield = array();
while (($csv = fgetcsv($handle, 0, variable_get('tablefield_csv_separator', ','))) !== FALSE) {
$col_count = count($csv);
foreach ($csv as $col_id => $col) {
$imported_tablefield['row_' . $row_count]['col_' . $col_id] = tablefield_convert_encoding($col, $encoding);
}
$max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
$row_count++;
}
fclose($handle);
ini_set('auto_detect_line_endings', $auto_detect_line_endings);
$imported_tablefield['rebuild'] = array(
'count_cols' => $max_col_count,
'count_rows' => $row_count,
);
drupal_array_set_nested_value($form_state['values'], $tablefield_parents, $imported_tablefield);
drupal_array_set_nested_value($form_state['input'], $tablefield_parents, $imported_tablefield);
drupal_set_message(t('Successfully imported @file', array(
'@file' => $file->filename,
)));
}
else {
drupal_set_message(t('There was a problem importing @file.', array(
'@file' => $file->filename,
)));
}
}
}
/**
* Helper function to import pasted data.
*
* @param array $form
* The form structure.
* @param array $form_state
* The current state of the form.
* @param string $langcode
* The language associated with the form items.
* @param array $tablefield_parents
* The parents of the tablefield element.
*/
function tablefield_import_pasted($form, &$form_state, $langcode, $pasted_form_field_name, $tablefield_parents) {
$data = $form_state['input']['data'][$pasted_form_field_name];
if (!empty($data)) {
// Empty the current table.
tablefield_delete_table_values(drupal_array_get_nested_value($form_state['values'], $tablefield_parents));
tablefield_delete_table_values(drupal_array_get_nested_value($form_state['input'], $tablefield_parents));
// Get the delimiter.
$col_delimiter = $form_state['input']['delimiter'][$pasted_form_field_name];
if ($col_delimiter == 'TAB') {
$col_delimiter = "\t";
}
// Populate table with pasted values.
$max_col_count = 0;
$row_count = 0;
$imported_tablefield = array();
$rows = explode(PHP_EOL, $data);
foreach ($rows as $row_id => $row) {
// Explode the current row into columns:
$cols = str_getcsv($row, $col_delimiter);
$col_count = count($cols);
if ($col_count > 0) {
foreach ($cols as $col_id => $col) {
$imported_tablefield['row_' . $row_count]['col_' . $col_id] = $col;
}
$max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
$row_count++;
}
}
// Rebuild the table if necessary, and save.
$imported_tablefield['rebuild'] = array(
'count_cols' => $max_col_count,
'count_rows' => $row_count,
);
drupal_array_set_nested_value($form_state['values'], $tablefield_parents, $imported_tablefield);
drupal_array_set_nested_value($form_state['input'], $tablefield_parents, $imported_tablefield);
drupal_set_message(t('Successfully imported pasted data.'));
}
else {
drupal_set_message(t('There was a problem importing pasted data.'));
}
}
/**
* Helper function to remove all values in a particular table.
*
* @param array $tablefield
* The table as it appears in FAPI.
*/
function tablefield_delete_table_values(&$tablefield) {
// Empty out previously entered values.
foreach ($tablefield as $key => $value) {
if (strpos($key, 'row_') === 0) {
$tablefield[$key] = '';
}
}
}
/**
* AJAX callback to rebuild the number of rows/columns.
*
* The basic idea is to descend down the list of #parent elements of the
* clicked_button in order to locate the tablefield inside of the $form array.
* That is the element that we need to return.
*/
function tablefield_rebuild_form_ajax($form, $form_state) {
$parents = $form_state['triggering_element']['#array_parents'];
// We do not want to go as deep as rebuild/rebuild or import/import,
// i.e. the triggering buttons.
array_pop($parents);
array_pop($parents);
$rebuild = drupal_array_get_nested_value($form, $parents);
// We don't want to re-send the format/_weight options.
unset($rebuild['format']);
unset($rebuild['_weight']);
// We need to avoid sending headers or the multipart form
// will make it fail. So, we need to explicitly define the
// whole response to ajax_deliver().
return array(
'#type' => 'ajax',
'#header' => FALSE,
'#commands' => array(
ajax_command_insert(NULL, drupal_render($rebuild)),
ajax_command_prepend(NULL, theme('status_messages')),
),
);
}
/**
* Helper function to rebuild the table structure without submitting the form.
*/
function tablefield_rebuild_form($form, &$form_state) {
// Maintain the tablefield data.
$form_state['tablefield_rebuild'] = $form_state['input'];
$form_state['rebuild'] = TRUE;
}
/**
* Implements hook_theme().
*/
function tablefield_theme() {
return array(
'tablefield_view' => array(
'variables' => array(
'caption' => NULL,
'header' => NULL,
'rows' => NULL,
'export' => NULL,
'delta' => NULL,
'entity_type' => NULL,
'entity_id' => NULL,
'field_name' => NULL,
'langcode' => NULL,
'formatter' => NULL,
'header_orientation' => 'Horizontal',
'sticky' => NULL,
'striping' => NULL,
'sortable' => NULL,
'table_custom_class_attributes' => NULL,
'attributes' => array(),
),
),
'tablefield' => array(
'variables' => array(
'rows' => NULL,
'attributes' => array(),
'caption' => NULL,
'colgroups' => array(),
'header_orientation' => 'Horizontal',
'sticky' => NULL,
'striping' => NULL,
'sortable' => NULL,
'table_custom_class_attributes' => NULL,
'empty' => NULL,
),
),
);
}
/**
* Theme function for table view.
*/
function theme_tablefield_view($variables) {
$id = $variables['entity_type'] . '-' . $variables['entity_id'] . '-' . $variables['field_name'] . '-' . $variables['delta'];
$sortable = $variables['sortable'] ? 'tablesorter' : NULL;
$cols_class = isset($variables['header']) ? 'tablefield-columns-' . count($variables['header']) : NULL;
$attributes = array(
'id' => 'tablefield-' . $id,
'class' => array(
'tablefield',
$sortable,
$cols_class,
),
);
// Apply scope property to headers for accessibility.
if (is_array($variables['header'])) {
foreach ($variables['header'] as &$header) {
$header['scope'] = 'col';
}
}
// If the user has access to the csv export option, display it now.
$export = '';
if ($variables['export'] && user_access('export tablefield')) {
$url = sprintf('tablefield/export/%s/%s/%s/%s/%s', $variables['entity_type'], $variables['entity_id'], $variables['field_name'], $variables['langcode'], $variables['delta']);
$export = '<div id="' . drupal_html_id('tablefield-export-link-' . $id) . '" class="tablefield-export-link">' . l(t('Export Table Data'), $url) . '</div>';
}
// Prepare variables for theme_tablefield().
$theme_variables = array(
'header' => $variables['header'],
'rows' => $variables['rows'],
'attributes' => $attributes,
'colgroups' => array(),
'empty' => NULL,
);
if (isset($variables['caption'])) {
$theme_variables['caption'] = $variables['caption'];
}
if (isset($variables['header_orientation'])) {
$theme_variables['header_orientation'] = $variables['header_orientation'];
}
if (isset($variables['sticky'])) {
$theme_variables['sticky'] = $variables['sticky'];
}
if (isset($variables['striping'])) {
$theme_variables['striping'] = $variables['striping'];
}
if (isset($variables['sortable'])) {
$theme_variables['sortable'] = $variables['sortable'];
}
if (!trim($variables['table_custom_class_attributes']) == FALSE) {
$theme_variables['attributes']['class'][] = $variables['table_custom_class_attributes'];
}
return '<div id="' . drupal_html_id('tablefield-wrapper-' . $id) . '" class="tablefield-wrapper">' . theme('tablefield', $theme_variables) . $export . '</div>';
}
/**
* Helper function to detect and convert strings not in UTF-8 to UTF-8.
*
* @param string $data
* The string which needs converting.
* @param string $encoding
* The encoding of the CSV file.
*
* @return string
* UTF encoded string.
*/
function tablefield_convert_encoding($data, $encoding) {
// Converting UTF-8 to UTF-8 will not work.
if ($encoding == 'UTF-8') {
return $data;
}
// Try convert the data to UTF-8.
if ($encoded_data = drupal_convert_to_utf8($data, $encoding)) {
return $encoded_data;
}
// Fallback on the input data.
return $data;
}
/**
* Trim trailing empty rows/columns.
*
* @param array $tabledata
* The rationalized tablefield.
* @param bool $ignore_head
* Whether ignoring header or not.
*/
function tablefield_trim($tabledata, $ignore_head = FALSE) {
$tabledata = array_reverse($tabledata);
// For columns the transposed array has the 'weight' one level higher.
unset($tabledata['weight']);
foreach ($tabledata as $key => $value) {
// Removes the weight key for the rows.
unset($value['weight']);
$empty = TRUE;
if (is_array($value)) {
foreach ($value as $k2 => $v2) {
if (!empty($v2)) {
// Stop traversing at the first non empty value.
if (!$ignore_head || $ignore_head && $k2 !== 'row_0') {
$empty = FALSE;
}
}
}
}
else {
if (!empty($value)) {
// Stop traversing at the first non empty value.
$empty = FALSE;
}
}
if ($empty) {
unset($tabledata[$key]);
}
else {
break;
}
}
$tabledata = array_reverse($tabledata);
return $tabledata;
}
/**
* Trim trailing empty columns.
*
* @param array $tabledata
* The rationalized tablefield.
* @param bool $ignore_head
* Whether ignoring header or not.
*/
function tablefield_rtrim_cols($tabledata, $ignore_head = FALSE) {
$row_num = !empty($tabledata) ? count($tabledata) : 0;
if (!$row_num) {
return $tabledata;
}
// Transpose the array.
$tabledata = tablefield_transpose($tabledata);
// Trim trailing empty rows.
$tabledata = tablefield_trim($tabledata, $ignore_head);
// Transpose back.
$tabledata = tablefield_transpose($tabledata);
return $tabledata;
}
/**
* Helper function to transpose table data arrays.
*/
function tablefield_transpose($array) {
$transposed = array();
foreach ($array as $key => $row) {
foreach ($row as $subkey => $value) {
$transposed[$subkey][$key] = $value;
}
}
return $transposed;
}
/**
* Hide all empty rows.
*
* @param array $tabledata
* The rationalized tablefield.
* @param bool $ignore_head
* Whether ignoring header or not.
*/
function tablefield_hide_rows($tabledata, $ignore_head = FALSE) {
foreach ($tabledata as $key => $value) {
// Removes the weight key.
unset($value['weight']);
$empty = TRUE;
if (is_array($value)) {
foreach ($value as $k2 => $v2) {
if (!empty($v2)) {
// Stop traversing at the first non empty value.
if (!$ignore_head || $ignore_head && $k2 !== 'row_0') {
$empty = FALSE;
}
}
}
}
else {
if (!empty($value)) {
// Stop traversing at the first non empty value.
$empty = FALSE;
}
}
if ($empty) {
unset($tabledata[$key]);
}
}
return $tabledata;
}
/**
* Hide all empty columns.
*
* @param array $tabledata
* The rationalized tablefield.
* @param bool $ignore_head
* Whether ignoring header or not.
*/
function tablefield_hide_cols($tabledata, $ignore_head = FALSE) {
$row_num = !empty($tabledata) ? count($tabledata) : 0;
if (!$row_num) {
return $tabledata;
}
// Transpose the array.
$tabledata = tablefield_transpose($tabledata);
// Trim trailing empty rows.
$tabledata = tablefield_hide_rows($tabledata, $ignore_head);
// Transpose back.
$tabledata = tablefield_transpose($tabledata);
return $tabledata;
}
/**
* Implements hook_multiple_field_remove_button_field_widgets_alter().
*
* Enable https://www.drupal.org/project/multiple_fields_remove_button.
*/
function tablefield_multiple_field_remove_button_field_widgets_alter(&$fieldwidgets) {
// Remove button for the following field type widgets.
$fieldwidgets[] = 'tablefield';
}
/**
* Implements hook_form_alter().
*
* Avoid empty tables on multivalue fields with default header values.
*/
function tablefield_form_alter(&$form, &$form_state, $form_id) {
if (empty($form_state['field'])) {
return;
}
foreach (element_children($form_state['field']) as $field_name) {
foreach ($form_state['field'][$field_name] as $lang => $value) {
if (isset($value['instance']) && $value['instance']['widget']['type'] === 'tablefield' && $value['field']['cardinality'] != 1) {
$key_exists = FALSE;
$max_delta = $form[$field_name][$lang]['#max_delta'];
$parents = array_merge($value['array_parents'], array(
$field_name,
$lang,
));
$element =& drupal_array_get_nested_value($form, $parents, $key_exists);
if ($key_exists && isset($element[$max_delta])) {
unset($element[$max_delta]);
}
}
}
}
}
/**
* Returns HTML for a table. Based on the core function theme_table().
*
* @param array $variables
* An associative array containing:
* - header: An array containing the table headers. Each element of the array
* can be either a localized string or an associative array with the
* following keys:
* - "data": The localized title of the table column.
* - "field": The database field represented in the table column (required
* if user is to be able to sort on this column).
* - "sort": A default sort order for this column ("asc" or "desc"). Only
* one column should be given a default sort order because table sorting
* only applies to one column at a time.
* - Any HTML attributes, such as "colspan", to apply to the column header
* cell.
* - rows: An array of table rows. Every row is an array of cells, or an
* associative array with the following keys:
* - "data": an array of cells
* - Any HTML attributes, such as "class", to apply to the table row.
* - "no_striping": a boolean indicating that the row should receive no
* 'even / odd' styling. Defaults to FALSE.
* Each cell can be either a string or an associative array with the
* following keys:
* - "data": The string to display in the table cell.
* - "header": Indicates this cell is a header.
* - Any HTML attributes, such as "colspan", to apply to the table cell.
* Here's an example for $rows:.
*
* @code
* $rows = array(
* // Simple row
* array(
* 'Cell 1', 'Cell 2', 'Cell 3'
* ),
* // Row with attributes on the row and some of its cells.
* array(
* 'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky')
* )
* );
* @endcode
* - attributes: An array of HTML attributes to apply to the table tag.
* - caption: A localized string to use for the <caption> tag.
* - colgroups: An array of column groups. Each element of the array can be
* either:
* - An array of columns, each of which is an associative array of HTML
* attributes applied to the COL element.
* - An array of attributes applied to the COLGROUP element, which must
* include a "data" attribute. To add attributes to COL elements, set the
* "data" attribute with an array of columns, each of which is an
* associative array of HTML attributes.
* Here's an example for $colgroup:
* @code
* $colgroup = array(
* // COLGROUP with one COL element.
* array(
* array(
* 'class' => array('funky'), // Attribute for the COL element.
* ),
* ),
* // Colgroup with attributes and inner COL elements.
* array(
* 'data' => array(
* array(
* 'class' => array('funky'), // Attribute for the COL element.
* ),
* ),
* 'class' => array('jazzy'), // Attribute for the COLGROUP element.
* ),
* );
* @endcode
* These optional tags are used to group and set properties on columns
* within a table. For example, one may easily group three columns and
* apply same background style to all.
* - header_orientation: Horizontal, vertical or both.
* - sticky: Use a "sticky" table header.
* - striping: Do add an 'odd' or 'even' class to rows.
* - sortable: Allow column sorting by clicking the column in the first row.
* - empty: The message to display in an extra row if table does not have any
* rows.
*/
function theme_tablefield($variables) {
$header = $variables['header'];
$rows = $variables['rows'];
$attributes = $variables['attributes'];
$caption = $variables['caption'];
$colgroups = $variables['colgroups'];
$header_orientation = $variables['header_orientation'];
$sticky = $variables['sticky'];
$table_custom_class_attributes = $variables['table_custom_class_attributes'];
$no_striping = !$variables['striping'];
$empty = $variables['empty'];
if ($variables['sortable']) {
drupal_add_css(drupal_get_path('module', 'tablefield') . '/css/tablefield_sort.css');
$tooltip = ' title="' . t('Sort column') . '"';
}
else {
$tooltip = NULL;
}
drupal_add_css(drupal_get_path('module', 'tablefield') . '/css/tablefield_sort.css');
$empty = $variables['empty'];
// If the only header is the first column make the first row a normal row.
if (!isset($rows['row_0']) && $header_orientation == 'Vertical') {
$rows['row_0'] = $header;
ksort($rows);
$header = array();
}
// Add sticky headers, if applicable.
if (!empty($header) && $sticky) {
drupal_add_js('misc/tableheader.js');
// Add 'sticky-enabled' class to the table to identify it for JS.
// This is needed to target tables constructed by this function.
$attributes['class'][] = 'sticky-enabled';
}
$output = '<table' . drupal_attributes($attributes) . ">\n";
if (isset($caption)) {
$output .= '<caption>' . $caption . "</caption>\n";
}
// Format the table columns:
if (!empty($colgroups)) {
foreach ($colgroups as $number => $colgroup) {
$attributes = array();
// Check if we're dealing with a simple or complex column.
if (isset($colgroup['data'])) {
foreach ($colgroup as $key => $value) {
if ($key == 'data') {
$cols = $value;
}
else {
$attributes[$key] = $value;
}
}
}
else {
$cols = $colgroup;
}
// Build colgroup.
if (is_array($cols) && !empty($cols)) {
$output .= ' <colgroup' . drupal_attributes($attributes) . '>';
$i = 0;
foreach ($cols as $col) {
$output .= ' <col' . drupal_attributes($col) . ' />';
}
$output .= " </colgroup>\n";
}
else {
$output .= ' <colgroup' . drupal_attributes($attributes) . " />\n";
}
}
}
// Add the 'empty' row message if available.
if (empty($rows) && $empty) {
$header_count = 0;
foreach ($header as $header_cell) {
if (is_array($header_cell)) {
$header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1;
}
else {
$header_count++;
}
}
$rows[] = array(
array(
'data' => $empty,
'colspan' => $header_count,
'class' => array(
'empty',
'message',
),
),
);
}
// Format the table header:
if (!empty($header)) {
$ts = tablesort_init($header);
// HTML requires that the thead tag has tr tags in it followed by tbody
// tags. Using ternary operator to check and see if we have any rows.
$output .= !empty($rows) ? ' <thead' . $tooltip . '><tr>' : ' <tr>';
foreach ($header as $cell) {
$cell = tablesort_header($cell, $header, $ts);
$output .= _theme_table_cell($cell, TRUE);
}
// Using ternary operator to close the tags based on whether or not there
// are rows.
$output .= !empty($rows) ? " </tr></thead>\n" : "</tr>\n";
}
else {
$ts = array();
}
// Format the table rows:
if (!empty($rows)) {
$output .= "<tbody>\n";
$flip = array(
'even' => 'odd',
'odd' => 'even',
);
$class = 'even';
foreach ($rows as $number => $row) {
// Check if we're dealing with a simple or complex row.
if (isset($row['data'])) {
$cells = $row['data'];
$no_striping = isset($row['no_striping']) ? $row['no_striping'] : FALSE;
// Set the attributes array and exclude 'data' and 'no_striping'.
$attributes = $row;
unset($attributes['data']);
unset($attributes['no_striping']);
}
else {
$cells = $row;
$attributes = array();
}
if (!empty($cells)) {
// Add odd/even class.
if (!$no_striping) {
$class = $flip[$class];
$attributes['class'][] = $class;
}
// Build row.
$output .= ' <tr' . drupal_attributes($attributes) . '>';
$i = 0;
foreach ($cells as $cell) {
$cell = tablesort_cell($cell, $header, $ts, $i++);
$output .= _theme_table_cell($cell, ($header_orientation === 'Both' || $header_orientation === 'Vertical') && $i == 1);
$i++;
}
$output .= " </tr>\n";
}
}
$output .= "</tbody>\n";
}
$output .= "</table>\n";
return $output;
}
/**
* Convert array to XML. See http://stackoverflow.com/a/5965940/523688.
*/
function array_to_xml($data, &$xml_data) {
foreach ($data as $key => $value) {
if (is_numeric($key)) {
// Dealing with <0/>..<n/> issues.
$key = 'item_' . $key;
}
// Within keys (XML element tags) only accept A-Z (case insensitive), 0-9,
// a dash (-) and an underscore (_). Replace all others with an underscore.
// Avoid multiple consecutive underscores.
$key = preg_replace('/_+/', '_', preg_replace('/[^A-Za-z0-9_\\-]/', '_', $key));
// If the <3element> starts with a number, prefix it with an underscore
// <_3element> to get valid XML.
$key = is_numeric(substr($key, 0, 1)) ? substr_replace($key, '_', 0, 0) : $key;
if (is_array($value)) {
$subnode = $xml_data
->addChild($key);
array_to_xml($value, $subnode);
}
else {
$xml_data
->addChild("{$key}", htmlspecialchars("{$value}"));
}
}
}
/**
* Check if $_POST contains more input variables than max_input_vars.
*
* @return bool
* TRUE if $_POST request variable contains more input variables than the
* max_input_vars configuration on the server. FALSE otherwise.
*/
function tablefield_post_exceeds_max_input_vars() {
$posted_vars = count($_POST, COUNT_RECURSIVE);
$max_input_vars = ini_get('max_input_vars');
return $posted_vars > $max_input_vars;
}
Functions
Name | Description |
---|---|
array_to_xml | Convert array to XML. See http://stackoverflow.com/a/5965940/523688. |
tablefield_admin_settings_form | Menu callback to prepare administration configuration form. |
tablefield_after_build | Form #after_build callback for tablefield_field_widget_form(). |
tablefield_convert_encoding | Helper function to detect and convert strings not in UTF-8 to UTF-8. |
tablefield_custom_theme | Implements hook_custom_theme(). |
tablefield_delete_table_values | Helper function to remove all values in a particular table. |
tablefield_export_csv | Menu callback to export a table as a CSV. |
tablefield_field_formatter_info | Implements hook_field_formatter_info(). |
tablefield_field_formatter_settings_form | Implements hook_field_formatter_settings_form(). |
tablefield_field_formatter_settings_summary | Implements hook_field_formatter_settings_summary(). |
tablefield_field_formatter_view | Implements hook_field_formatter_view(). |
tablefield_field_info | Implements hook_field_info(). |
tablefield_field_is_empty | Implements hook_field_is_empty(). |
tablefield_field_load | Implements hook_field_load(). |
tablefield_field_prepare_view | Implements hook_field_prepare_view(). |
tablefield_field_presave | Implements hook_field_presave(). |
tablefield_field_settings_form | Implements hook_field_settings_form(). |
tablefield_field_validate | Implements hook_field_validate(). |
tablefield_field_widget_error | Implements hook_field_widget_error(). |
tablefield_field_widget_form | Implements hook_field_widget_form(). |
tablefield_field_widget_form_validate | Form validation callback. |
tablefield_field_widget_info | Implements hook_field_widget_info(). |
tablefield_field_widget_settings_form | Implements hook_field_widget_settings_form(). |
tablefield_form_alter | Implements hook_form_alter(). |
tablefield_get_table_value | Get the property just as it is set in the data. Search API indexing addition. |
tablefield_help | Implements hook_help(). |
tablefield_hide_cols | Hide all empty columns. |
tablefield_hide_rows | Hide all empty rows. |
tablefield_import_csv | Helper function to import data from a CSV file. |
tablefield_import_pasted | Helper function to import pasted data. |
tablefield_item_property_info | Define metadata about item properties. Search API indexing addition. |
tablefield_menu | Implements hook_menu(). |
tablefield_multiple_field_remove_button_field_widgets_alter | Implements hook_multiple_field_remove_button_field_widgets_alter(). |
tablefield_permission | Implements hook_permission(). |
tablefield_post_exceeds_max_input_vars | Check if $_POST contains more input variables than max_input_vars. |
tablefield_property_info_callback | Defines info for the properties of the tablefield field data structure. |
tablefield_rebuild_form | Helper function to rebuild the table structure without submitting the form. |
tablefield_rebuild_form_ajax | AJAX callback to rebuild the number of rows/columns. |
tablefield_replace_csv_values | Replace table field values with the values that were saved. |
tablefield_rtrim_cols | Trim trailing empty columns. |
tablefield_tablefield_to_array | Translate tablefield to associative array. |
tablefield_theme | Implements hook_theme(). |
tablefield_transpose | Helper function to transpose table data arrays. |
tablefield_trim | Trim trailing empty rows/columns. |
tablefield_validate_number | Form element validation handler for #type 'tablefield_number'. |
tablefield_valid_number_step | Verifies that a number is a multiple of a given step. |
tablefield_value_array_get | Get the value property in formatted array. |
theme_tablefield | Returns HTML for a table. Based on the core function theme_table(). |
theme_tablefield_view | Theme function for table view. |
_cell_validate | Custom callback for a textarea to be processed for linebreaks. |
_tablefield_entity_access | Helper function: A sort-of copy of entity_access() from entity API. |