diff --git a/src/Element/Tablefield.php b/src/Element/Tablefield.php index 89ea705..1002bd1 100644 --- a/src/Element/Tablefield.php +++ b/src/Element/Tablefield.php @@ -28,6 +28,7 @@ class Tablefield extends FormElement { '#input_type' => 'textfield', '#rebuild' => FALSE, '#import' => FALSE, + '#editor' => FALSE, '#paste' => FALSE, '#process' => [ [$class, 'processTablefield'], @@ -108,24 +109,35 @@ class Tablefield extends FormElement { $draggable = TRUE; for ($ii = 0; $ii < $cols; $ii++) { if (!empty($element['#locked_cells'][$i][$ii]) && !empty($element['#lock'])) { + if (is_array($element['#locked_cells'][$i][$ii]) && isset($element['#locked_cells'][$i][$ii]['value']) && isset($element['#locked_cells'][$i][$ii]['format'])) { + $cell_value = $element['#locked_cells'][$i][$ii]['value']; + $cell_format = $element['#locked_cells'][$i][$ii]['format']; + } + else { + $cell_value = $element['#locked_cells'][$i][$ii]; + $cell_format = NULL; + } + $cell_rendered = !empty($cell_format) ? check_markup($cell_value, $cell_format) : $cell_value; $draggable = FALSE; $weightedRows[$i][$ii] = [ '#type' => 'item', - '#value' => $element['#locked_cells'][$i][$ii], - '#title' => $element['#locked_cells'][$i][$ii], + '#value' => $cell_rendered, + '#title' => $cell_rendered, ]; } else { $cell_value = $value[$i][$ii] ?? ''; + $cell_rendered = is_array($cell_value) && isset($cell_value['value']) ? $cell_value['value'] : $cell_value; $weightedRows[$i][$ii] = [ - '#type' => $input_type, + '#type' => $element['#editor'] ? 'text_format' : $input_type, '#maxlength' => 2048, '#size' => 0, '#attributes' => [ 'class' => ['tablefield-row-' . $i, 'tablefield-col-' . $ii], 'style' => 'width:100%', ], - '#default_value' => $cell_value, + '#default_value' => $cell_rendered, + '#format' => is_array($cell_value) && isset($cell_value['format']) ? $cell_value['format'] : NULL, ]; } } @@ -345,8 +357,7 @@ class Tablefield extends FormElement { $parents = array_slice($triggering_element['#array_parents'], 0, -2, TRUE); $rebuild = NestedArray::getValue($form, $parents); - // We don't want to re-send the format/_weight options. - unset($rebuild['format']); + // We don't want to re-send the _weight option. unset($rebuild['_weight']); // Set row value to default only if there is Add Row button clicked. @@ -388,6 +399,25 @@ class Tablefield extends FormElement { $imported_tablefield = static::importCsv($id); if ($imported_tablefield) { + $array_parents = array_slice($triggering_element['#array_parents'], 0, -2, TRUE); + $element = NestedArray::getValue($form, $array_parents); + + $array_element_parents = array_slice($triggering_element['#array_parents'], 0, -3, TRUE); + $element_parents = NestedArray::getValue($form, $array_element_parents); + + $value = $imported_tablefield['table']; + foreach ($value as $row => $row_value) { + foreach ($row_value as $col => $col_value) { + $col_el = $element['table'][$row][$col]; + if ((empty($col_el) && $element_parents['#editor']) || (!empty($col_el) && $col_el['#type'] == 'text_format')) { + $imported_tablefield['table'][$row][$col] = [ + 'format' => $col_el['#format'], + 'value' => $col_value + ]; + } + } + } + $form_state->setValue($parents, $imported_tablefield); $input = $form_state->getUserInput(); diff --git a/src/Plugin/Field/FieldFormatter/TablefieldFormatter.php b/src/Plugin/Field/FieldFormatter/TablefieldFormatter.php index d2555a4..0947d6f 100644 --- a/src/Plugin/Field/FieldFormatter/TablefieldFormatter.php +++ b/src/Plugin/Field/FieldFormatter/TablefieldFormatter.php @@ -155,8 +155,10 @@ class TablefieldFormatter extends FormatterBase implements ContainerFactoryPlugi foreach ($tabledata as $row_key => $row) { foreach ($row as $col_key => $cell) { if (is_numeric($col_key)) { + $value = is_array($cell) && isset($cell['value']) ? $cell['value'] : $cell; + $format = is_array($cell) && isset($cell['format']) ? $cell['format'] : NULL; $tabledata[$row_key][$col_key] = [ - 'data' => empty($table->format) ? $cell : check_markup($cell, $table->format), + 'data' => empty($format) ? $value : check_markup($value, $format), 'class' => ['row_' . $row_key, 'col_' . $col_key], ]; } diff --git a/src/Plugin/Field/FieldType/TablefieldItem.php b/src/Plugin/Field/FieldType/TablefieldItem.php index 38fc2c9..c6ba579 100644 --- a/src/Plugin/Field/FieldType/TablefieldItem.php +++ b/src/Plugin/Field/FieldType/TablefieldItem.php @@ -35,15 +35,10 @@ class TablefieldItem extends FieldItemBase { 'size' => 'big', 'serialize' => TRUE, ], - 'format' => [ - 'type' => 'varchar', - 'length' => 255, - 'default value' => '', - ], 'caption' => [ 'type' => 'varchar', 'length' => 255, - 'default value' => '', + 'default' => '', ], ], ]; @@ -141,9 +136,6 @@ class TablefieldItem extends FieldItemBase { ->setLabel(t('Table data')) ->setDescription(t('Stores tabular data.')); - $properties['format'] = DataDefinition::create('filter_format') - ->setLabel(t('Text format')); - $properties['caption'] = DataDefinition::create('string') ->setLabel(t('Table Caption')); diff --git a/src/Plugin/Field/FieldWidget/TablefieldWidget.php b/src/Plugin/Field/FieldWidget/TablefieldWidget.php index 8eb5ba0..7541a34 100644 --- a/src/Plugin/Field/FieldWidget/TablefieldWidget.php +++ b/src/Plugin/Field/FieldWidget/TablefieldWidget.php @@ -176,10 +176,7 @@ class TablefieldWidget extends WidgetBase implements ContainerFactoryPluginInter // Allow the user to select input filters. if (!empty($field_settings['cell_processing'])) { - $element['#base_type'] = $element['#type']; - $element['#type'] = 'text_format'; - $element['#format'] = $default_value->format ?? NULL; - $element['#editor'] = FALSE; + $element['#editor'] = TRUE; } return $element; diff --git a/tablefield.install b/tablefield.install index d5ea4fb..1e2e7de 100644 --- a/tablefield.install +++ b/tablefield.install @@ -5,6 +5,8 @@ * Installation options for TableField. */ +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; + /** * Add columns for caption field to the database. */ @@ -13,39 +15,149 @@ function tablefield_update_8001() { 'type' => 'varchar', 'length' => 255, 'default' => '', - 'not null' => TRUE, + 'not null' => FALSE, ]); } +/** + * Remove "format" column because each cell will use its own format. + */ +function tablefield_update_8002() { + tablefield_remove_existing_column('format'); +} + /** * Helper function to add new columns to the field schema. * - * @param string $column_name + * @param string $new_property_name * The name of the column that will be added. * @param array $spec * The options of the new column. */ -function tablefield_add_new_column($column_name, array $spec) { - $field_map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('tablefield'); +function tablefield_add_new_column(string $new_property_name, array $spec) { $schema = \Drupal::database()->schema(); + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_field_manager = \Drupal::service('entity_field.manager'); + $entity_field_map = $entity_field_manager->getFieldMapByFieldType('tablefield'); + $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql'); + /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */ + $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); - foreach ($field_map as $entity_type_id => $fields) { - foreach (array_keys($fields) as $field_name) { - $tables = [ - "{$entity_type_id}__$field_name", - "{$entity_type_id}_revision__$field_name", - ]; + foreach ($entity_field_map as $entity_type_id => $fields) { + $entity_storage = $entity_type_manager->getStorage($entity_type_id); + if (!$entity_storage instanceof SqlEntityStorageInterface) { + continue; + } + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + // Loads definitions for all fields. + $entity_field_storage_defintitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_storage->getTableMapping($entity_field_storage_defintitions); - $new_column_name = $field_name . '_' . $column_name; + // Intersect tablefield fields with storage definitions for all + // fields. + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_definitions */ + $field_definitions = array_intersect_key($entity_field_storage_defintitions, $fields); + + // Iterate over all tablefield field definitions for this entity type. + foreach ($field_definitions as $field_definition) { + $field_name = $field_definition->getName(); + $tables = []; + $tables[] = $table_mapping->getFieldTableName($field_name); + if ($entity_type->isRevisionable() && $field_definition->isRevisionable()) { + $tables[] = $table_mapping->getDedicatedRevisionTableName($field_definition); + } + + // Field type column names map to real table column names. + $columns = $table_mapping->getColumnNames($field_name); + $column_name = $columns[$new_property_name]; foreach ($tables as $table) { - $field_exists = $schema->fieldExists($table, $new_column_name); - $table_exists = $schema->tableExists($table); + if (!$schema->fieldExists($table, $column_name)) { + $schema->addField($table, $column_name, $spec); + } + } + + // Update the tracked entity table schema. + $schema_key = "$entity_type_id.field_schema_data.$field_name"; + $field_schema_data = $entity_storage_schema_sql->get($schema_key); + foreach ($field_schema_data as $table_name => $field_schema) { + // Remove the column from the field schema data. + $field_schema_data[$table_name]['fields'][$column_name] = $spec; + } + $entity_storage_schema_sql->set($schema_key, $field_schema_data); + + $definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id); + $definitions[$field_name] = $field_definition; + $last_installed_schema_repository->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); + } + } +} + +/** + * Helper function to remove columns from the field schema. + * + * @param string $property_to_remove + * The name of the field that will be added. + */ +function tablefield_remove_existing_column(string $property_to_remove) { + $schema = \Drupal::database()->schema(); + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_field_manager = \Drupal::service('entity_field.manager'); + $entity_field_map = $entity_field_manager->getFieldMapByFieldType('tablefield'); + $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql'); + /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */ + $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); + + foreach ($entity_field_map as $entity_type_id => $fields) { + $entity_storage = $entity_type_manager->getStorage($entity_type_id); + if (!$entity_storage instanceof SqlEntityStorageInterface) { + continue; + } + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + // Loads definitions for all fields. + $entity_field_storage_defintitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_storage->getTableMapping($entity_field_storage_defintitions); - if (!$field_exists && $table_exists) { - $schema->addField($table, $new_column_name, $spec); + // Intersect tablefield fields with storage definitions for all + // fields. + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_definitions */ + $field_definitions = array_intersect_key($entity_field_storage_defintitions, $fields); + + // Iterate over all tablefield field definitions for this entity type. + foreach ($field_definitions as $field_definition) { + $field_name = $field_definition->getName(); + $tables = []; + $tables[] = $table_mapping->getFieldTableName($field_name); + if ($entity_type->isRevisionable() && $field_definition->isRevisionable()) { + $tables[] = $table_mapping->getDedicatedRevisionTableName($field_definition); + } + + $column_name = $field_name . '_' . $property_to_remove; + + foreach ($tables as $table) { + if ($schema->fieldExists($table, $column_name)) { + $schema->dropField($table, $column_name); } } + + // Update the tracked entity table schema. + $schema_key = "$entity_type_id.field_schema_data.$field_name"; + $field_schema_data = $entity_storage_schema_sql->get($schema_key); + foreach ($field_schema_data as $table_name => $field_schema) { + // Remove the column from the field schema data. + unset($field_schema_data[$table_name]['fields'][$column_name]); + } + $entity_storage_schema_sql->set($schema_key, $field_schema_data); + + $definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id); + $definitions[$field_name] = $field_definition; + $last_installed_schema_repository->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); } } } diff --git a/tablefield.post_update.php b/tablefield.post_update.php index c3c8e90..436b6ea 100644 --- a/tablefield.post_update.php +++ b/tablefield.post_update.php @@ -40,7 +40,7 @@ function tablefield_post_update_implement_tablefield_entity_view_display_schema( ->update($sandbox, 'entity_view_display', function (EntityViewDisplayInterface $entityViewDisplay) { $updated = FALSE; foreach ($entityViewDisplay->getComponents() as $key => $component) { - if ($component['type'] === 'tablefield') { + if (isset($component['type']) && $component['type'] === 'tablefield') { $component['settings']['row_header'] = (bool) $component['settings']['row_header']; $component['settings']['column_header'] = (bool) $component['settings']['column_header']; $entityViewDisplay->setComponent($key, $component); diff --git a/tests/src/Functional/TableValueFieldTest.php b/tests/src/Functional/TableValueFieldTest.php index 8264180..70d66a9 100644 --- a/tests/src/Functional/TableValueFieldTest.php +++ b/tests/src/Functional/TableValueFieldTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\tablefield\Functional; +use Drupal\field\Entity\FieldConfig; use Drupal\Tests\BrowserTestBase; /** @@ -21,7 +22,7 @@ class TableValueFieldTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected static $modules = ['node', 'tablefield']; + protected static $modules = ['node', 'tablefield', 'filter_test']; /** * {@inheritdoc} @@ -75,4 +76,42 @@ class TableValueFieldTest extends BrowserTestBase { $assert_session->elementContains('css', 'table#tablefield-node-1-field_table-0 tbody tr td.row_2.col_2', 'Row 2-3'); } + /** + * Create a node with with a tablefield and cell processing enabled. + */ + public function testTablefieldCellProcessing() { + // Enable cell processing. + $field_config = FieldConfig::loadByName('node', 'article', 'field_table'); + $field_config->setSetting('cell_processing', TRUE)->save(); + + $this->drupalGet('node/add/article'); + $this->submitForm([ + 'title[0][value]' => 'Llamas are cool', + 'field_table[0][tablefield][table][0][0][value]' => 'Bold text', + 'field_table[0][tablefield][table][0][0][format]' => 'filtered_html', + 'field_table[0][tablefield][table][0][1][value]' => 'Forbidden HTML', + 'field_table[0][tablefield][table][0][1][format]' => 'filtered_html', + 'field_table[0][tablefield][table][0][2][value]' => 'Underlined text.', + 'field_table[0][tablefield][table][0][2][format]' => 'full_html', + ], 'Save'); + + $assert_session = $this->assertSession(); + $assert_session->pageTextContains('Article Llamas are cool has been created.'); + + // The tag is allowed by the filtered_html format, so this should + // be shown. + $assert_session->elementContains('css', 'table#tablefield-node-1-field_table-0 thead th.row_0.col_0', 'Bold text'); + // The tag is not allowed by the filtered_html format, so this tag + // should be removed. + $assert_session->elementContains('css', 'table#tablefield-node-1-field_table-0 thead th.row_0.col_1', 'Forbidden'); + // All HTML is allowed by the full_html format. + $assert_session->elementContains('css', 'table#tablefield-node-1-field_table-0 thead th.row_0.col_2', 'Underlined text.'); + + // Check that the submitted data is correctly saved in the database. + $node = $this->drupalGetNodeByTitle('Llamas are cool'); + $this->assertEquals(['value' => 'Bold text', 'format' => 'filtered_html'], $node->get('field_table')->value[0][0]); + $this->assertEquals(['value' => 'Forbidden HTML', 'format' => 'filtered_html'], $node->get('field_table')->value[0][1]); + $this->assertEquals(['value' => 'Underlined text.', 'format' => 'full_html'], $node->get('field_table')->value[0][2]); + } + }