first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
.field-icon-formatted_text {
background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m21 9.48v1.5h15v-3h-15zm-20.236 1.425c-.388 1.576-.705 2.913-.704 2.972 0 .097.062.105.698.09l.699-.017.459-.99c.567-1.222.894-1.777 1.322-2.241.813-.88 1.883-1.239 3.696-1.239h.579l-.04.165c-.449 1.841-4.58 17.152-4.673 17.32a1.696 1.696 0 0 1 -.362.42l-.231.185-1.104-.017-1.103-.016v1.443h10.5v-.716c0-.696-.004-.716-.135-.738-.074-.012-.676-.01-1.337.005-1.038.022-1.228.012-1.395-.074-.191-.099-.193-.105-.193-.542 0-.641.125-1.135 2.45-9.695l2.095-7.71.892.006c1.115.008 1.444.091 1.871.474.782.703 1.239 1.865 1.362 3.465l.041.525h1.849v-5.94h-16.532zm15.736 7.605v1.47h19.5v-2.94h-19.5zm-3 9v1.47h22.5v-2.94h-22.5z' fill='%2355565b'/%3e%3c/svg%3e");
}

View File

@@ -0,0 +1,3 @@
.field-icon-formatted_text {
background-image: url(../../../misc/icons/55565b/formatted_text.svg);
}

View File

@@ -0,0 +1,5 @@
finished:
6:
text: text
7:
text: text

View File

@@ -0,0 +1,16 @@
id: text_settings
label: Drupal teaser length configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- teaser_length
source_module: text
process:
default_summary_length: teaser_length
destination:
plugin: config
config_name: text.settings

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\text\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'text_default' formatter.
*/
#[FieldFormatter(
id: 'text_default',
label: new TranslatableMarkup('Default'),
field_types: [
'text',
'text_long',
'text_with_summary',
],
)]
class TextDefaultFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
// The ProcessedText element already handles cache context & tag bubbling.
// @see \Drupal\filter\Element\ProcessedText::preRenderText()
foreach ($items as $delta => $item) {
$elements[$delta] = [
'#type' => 'processed_text',
'#text' => $item->value,
'#format' => $item->format,
'#langcode' => $item->getLangcode(),
];
}
return $elements;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\text\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'text_summary_or_trimmed' formatter.
*/
#[FieldFormatter(
id: 'text_summary_or_trimmed',
label: new TranslatableMarkup('Summary or trimmed'),
field_types: [
'text_with_summary',
],
)]
class TextSummaryOrTrimmedFormatter extends TextTrimmedFormatter {}

View File

@@ -0,0 +1,133 @@
<?php
namespace Drupal\text\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'text_trimmed' formatter.
*
* Note: This class also contains the implementations used by the
* 'text_summary_or_trimmed' formatter.
*
* @see \Drupal\text\Field\Formatter\TextSummaryOrTrimmedFormatter
*/
#[FieldFormatter(
id: 'text_trimmed',
label: new TranslatableMarkup('Trimmed'),
field_types: [
'text',
'text_long',
'text_with_summary',
],
)]
class TextTrimmedFormatter extends FormatterBase implements TrustedCallbackInterface {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'trim_length' => '600',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element['trim_length'] = [
'#title' => $this->t('Trimmed limit'),
'#type' => 'number',
'#field_suffix' => $this->t('characters'),
'#default_value' => $this->getSetting('trim_length'),
'#description' => $this->t('If the summary is not set, the trimmed %label field will end at the last full sentence before this character limit.', ['%label' => $this->fieldDefinition->getLabel()]),
'#min' => 1,
'#required' => TRUE,
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = $this->t('Trimmed limit: @trim_length characters', ['@trim_length' => $this->getSetting('trim_length')]);
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$render_as_summary = function (&$element) {
// Make sure any default #pre_render callbacks are set on the element,
// because text_pre_render_summary() must run last.
$element += \Drupal::service('element_info')->getInfo($element['#type']);
// Add the #pre_render callback that renders the text into a summary.
$element['#pre_render'][] = [TextTrimmedFormatter::class, 'preRenderSummary'];
// Pass on the trim length to the #pre_render callback via a property.
$element['#text_summary_trim_length'] = $this->getSetting('trim_length');
};
// The ProcessedText element already handles cache context & tag bubbling.
// @see \Drupal\filter\Element\ProcessedText::preRenderText()
foreach ($items as $delta => $item) {
$elements[$delta] = [
'#type' => 'processed_text',
'#text' => NULL,
'#format' => $item->format,
'#langcode' => $item->getLangcode(),
];
if ($this->getPluginId() == 'text_summary_or_trimmed' && !empty($item->summary)) {
$elements[$delta]['#text'] = $item->summary;
}
else {
$elements[$delta]['#text'] = $item->value;
$render_as_summary($elements[$delta]);
}
}
return $elements;
}
/**
* Pre-render callback: Renders a processed text element's #markup as a summary.
*
* @param array $element
* A structured array with the following key-value pairs:
* - #markup: the filtered text (as filtered by filter_pre_render_text())
* - #format: containing the machine name of the filter format to be used to
* filter the text. Defaults to the fallback format. See
* filter_fallback_format().
* - #text_summary_trim_length: the desired character length of the summary
* (used by text_summary())
*
* @return array
* The passed-in element with the filtered text in '#markup' trimmed.
*
* @see filter_pre_render_text()
* @see text_summary()
*/
public static function preRenderSummary(array $element) {
$element['#markup'] = text_summary($element['#markup'], $element['#format'], $element['#text_summary_trim_length']);
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderSummary'];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\text\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines an item list class for text fields.
*/
class TextFieldItemList extends FieldItemList {
/**
* {@inheritdoc}
*/
public function defaultValuesFormValidate(array $element, array &$form, FormStateInterface $form_state) {
if ($allowed_formats = $this->getSetting('allowed_formats')) {
$field_name = $this->definition->getName();
$submitted_values = $form_state->getValue([
'default_value_input',
$field_name,
]);
foreach ($submitted_values as $delta => $value) {
if (!in_array($value['format'], $allowed_formats, TRUE)) {
$form_state->setErrorByName(
"default_value_input][{$field_name}][{$delta}][format",
$this->t("The selected text format is not allowed.")
);
}
}
}
parent::defaultValuesFormValidate($element, $form, $form_state);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Drupal\text\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'text' field type.
*/
#[FieldType(
id: "text",
label: new TranslatableMarkup("Text (formatted)"),
description: [
new TranslatableMarkup("Ideal for titles and names that need to support markup such as bold, italics or links"),
new TranslatableMarkup("Efficient storage for short text"),
new TranslatableMarkup("Requires specifying a maximum length"),
new TranslatableMarkup("Good for fields with known or predictable lengths"),
],
category: "formatted_text",
default_widget: "text_textfield",
default_formatter: "text_default",
list_class: TextFieldItemList::class,
)]
class TextItem extends TextItemBase {
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'max_length' => 255,
] + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'varchar',
'length' => $field_definition->getSetting('max_length'),
],
'format' => [
'type' => 'varchar',
'length' => 255,
],
],
'indexes' => [
'format' => ['format'],
],
];
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
$constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$constraints = parent::getConstraints();
if ($max_length = $this->getSetting('max_length')) {
$constraints[] = $constraint_manager->create('ComplexData', [
'value' => [
'Length' => [
'max' => $max_length,
'maxMessage' => $this->t('%name: the text may not be longer than @max characters.', ['%name' => $this->getFieldDefinition()->getLabel(), '@max' => $max_length]),
],
],
]);
}
return $constraints;
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = [];
$element['max_length'] = [
'#type' => 'number',
'#title' => $this->t('Maximum length'),
'#default_value' => $this->getSetting('max_length'),
'#required' => TRUE,
'#description' => $this->t('The maximum length of the field in characters.'),
'#min' => 1,
'#disabled' => $has_data,
];
$element += parent::storageSettingsForm($form, $form_state, $has_data);
return $element;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Drupal\text\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Base class for 'text' configurable field types.
*/
abstract class TextItemBase extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return ['allowed_formats' => []] + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = parent::fieldSettingsForm($form, $form_state);
$settings = $this->getSettings();
$element['allowed_formats'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Allowed text formats'),
'#options' => $this->get('format')->getPossibleOptions(),
'#default_value' => !empty($settings['allowed_formats']) ? $settings['allowed_formats'] : [],
'#description' => $this->t('Select the allowed text formats. If no formats are selected, all available text formats will be displayed to the user.'),
'#element_validate' => [[static::class, 'validateAllowedFormats']],
];
return $element;
}
/**
* Render API callback: Processes the allowed formats value.
*
* Ensure the element's value is an indexed array of selected format IDs.
* This function is assigned as an #element_validate callback.
*
* @see static::fieldSettingsForm()
*/
public static function validateAllowedFormats(array &$element, FormStateInterface $form_state) {
$value = array_values(array_filter($form_state->getValue($element['#parents'])));
$form_state->setValueForElement($element, $value);
}
/**
* {@inheritdoc}
*/
public static function calculateDependencies(FieldDefinitionInterface $field_definition) {
// Add explicitly allowed formats as config dependencies.
$format_dependencies = [];
$dependencies = parent::calculateDependencies($field_definition);
if (!is_null($field_definition->getSetting('allowed_formats'))) {
$format_dependencies = array_map(function (string $format_id) {
return 'filter.format.' . $format_id;
}, $field_definition->getSetting('allowed_formats'));
}
$config = $dependencies['config'] ?? [];
$dependencies['config'] = array_merge($config, $format_dependencies);
return $dependencies;
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Text'))
->setRequired(TRUE);
$properties['format'] = DataDefinition::create('filter_format')
->setLabel(t('Text format'))
->setSetting('allowed_formats', $field_definition->getSetting('allowed_formats'));
$properties['processed'] = DataDefinition::create('string')
->setLabel(t('Processed text'))
->setDescription(t('The text with the text format applied.'))
->setComputed(TRUE)
->setClass('\Drupal\text\TextProcessed')
->setSetting('text source', 'value')
->setInternal(FALSE);
return $properties;
}
/**
* {@inheritdoc}
*/
public function applyDefaultValue($notify = TRUE) {
// @todo Add in the filter default format here.
$this->setValue(['format' => NULL], $notify);
return $this;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}
/**
* {@inheritdoc}
*/
public function onChange($property_name, $notify = TRUE) {
// Unset processed properties that are affected by the change.
foreach ($this->definition->getPropertyDefinitions() as $property => $definition) {
if ($definition->getClass() == '\Drupal\text\TextProcessed') {
if ($property_name == 'format' || ($definition->getSetting('text source') == $property_name)) {
$this->writePropertyValue($property, NULL);
}
}
}
parent::onChange($property_name, $notify);
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$random = new Random();
$settings = $field_definition->getSettings();
if (empty($settings['max_length'])) {
// Textarea handling
$value = $random->paragraphs();
}
else {
// Textfield handling.
$max = ceil($settings['max_length'] / 3);
$value = substr($random->sentences(mt_rand(1, $max), FALSE), 0, $settings['max_length']);
}
$values = [
'value' => $value,
'summary' => $value,
'format' => filter_fallback_format(),
];
return $values;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\text\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'text_long' field type.
*/
#[FieldType(
id: "text_long",
label: new TranslatableMarkup("Text (formatted, long)"),
description: [
new TranslatableMarkup("Ideal for longer texts, like body or description without a summary"),
new TranslatableMarkup("Supports long text without specifying a maximum length"),
new TranslatableMarkup("May use more storage and be slower for searching and sorting"),
],
category: "formatted_text",
default_widget: "text_textarea",
default_formatter: "text_default",
list_class: TextFieldItemList::class,
)]
class TextLongItem extends TextItemBase {
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'text',
'size' => 'big',
],
'format' => [
'type' => 'varchar_ascii',
'length' => 255,
],
],
'indexes' => [
'format' => ['format'],
],
];
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace Drupal\text\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
/**
* Plugin implementation of the 'text_with_summary' field type.
*/
#[FieldType(
id: "text_with_summary",
label: new TranslatableMarkup("Text (formatted, long, with summary)"),
description: [
new TranslatableMarkup("Ideal for longer texts, like body or description with a summary"),
new TranslatableMarkup("Allows specifying a summary for the text"),
new TranslatableMarkup("Supports long text without specifying a maximum length"),
new TranslatableMarkup("May use more storage and be slower for searching and sorting"),
],
category: "formatted_text",
default_widget: "text_textarea_with_summary",
default_formatter: "text_default",
list_class: TextFieldItemList::class,
)]
class TextWithSummaryItem extends TextItemBase {
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return [
'display_summary' => 0,
'required_summary' => FALSE,
] + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$properties['summary'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Summary'));
$properties['summary_processed'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Processed summary'))
->setDescription(new TranslatableMarkup('The summary text with the text format applied.'))
->setComputed(TRUE)
->setClass('\Drupal\text\TextProcessed')
->setSetting('text source', 'summary');
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'text',
'size' => 'big',
],
'summary' => [
'type' => 'text',
'size' => 'big',
],
'format' => [
'type' => 'varchar_ascii',
'length' => 255,
],
],
'indexes' => [
'format' => ['format'],
],
];
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$value = $this->get('summary')->getValue();
return parent::isEmpty() && ($value === NULL || $value === '');
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = parent::fieldSettingsForm($form, $form_state);
$settings = $this->getSettings();
$element['display_summary'] = [
'#type' => 'checkbox',
'#title' => $this->t('Summary input'),
'#default_value' => $settings['display_summary'],
'#description' => $this->t('This allows authors to input an explicit summary, to be displayed instead of the automatically trimmed text when using the "Summary or trimmed" display type.'),
];
$element['required_summary'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require summary'),
'#description' => $this->t('The summary will also be visible when marked as required.'),
'#default_value' => $settings['required_summary'],
];
return $element;
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
$constraints = parent::getConstraints();
if ($this->getSetting('required_summary')) {
$manager = $this->getTypedDataManager()->getValidationConstraintManager();
$constraints[] = $manager->create('ComplexData', [
'summary' => [
'NotNull' => [
'message' => $this->t('The summary field is required for @name', ['@name' => $this->getFieldDefinition()->getLabel()]),
],
],
]);
}
return $constraints;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Drupal\text\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextareaWidget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Plugin implementation of the 'text_textarea' widget.
*/
#[FieldWidget(
id: 'text_textarea',
label: new TranslatableMarkup('Text area (multiple rows)'),
field_types: ['text_long'],
)]
class TextareaWidget extends StringTextareaWidget {
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
$element['rows']['#description'] = $this->t('Text editors may override this setting.');
return $element;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$main_widget = parent::formElement($items, $delta, $element, $form, $form_state);
$allowed_formats = $this->getFieldSetting('allowed_formats');
$element = $main_widget['value'];
$element['#type'] = 'text_format';
$element['#format'] = $items[$delta]->format;
$element['#base_type'] = $main_widget['value']['#type'];
if ($allowed_formats && !$this->isDefaultValueWidget($form_state)) {
$element['#allowed_formats'] = $allowed_formats;
}
return $element;
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
if (isset($element['format']['#access']) && !$element['format']['#access'] && preg_match('/^[0-9]*\.format$/', $violation->getPropertyPath())) {
// Ignore validation errors for formats if formats may not be changed,
// such as when existing formats become invalid.
// See \Drupal\filter\Element\TextFormat::processFormat().
return FALSE;
}
return $element;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Drupal\text\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'text_textarea_with_summary' widget.
*/
#[FieldWidget(
id: 'text_textarea_with_summary',
label: new TranslatableMarkup('Text area with a summary'),
field_types: ['text_with_summary'],
)]
class TextareaWithSummaryWidget extends TextareaWidget {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'rows' => '9',
'summary_rows' => '3',
'placeholder' => '',
'show_summary' => FALSE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
$element['summary_rows'] = [
'#type' => 'number',
'#title' => $this->t('Summary rows'),
'#default_value' => $this->getSetting('summary_rows'),
'#description' => $element['rows']['#description'],
'#required' => TRUE,
'#min' => 1,
];
$element['show_summary'] = [
'#type' => 'checkbox',
'#title' => $this->t('Always show the summary field'),
'#default_value' => $this->getSetting('show_summary'),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->t('Number of summary rows: @rows', ['@rows' => $this->getSetting('summary_rows')]);
if ($this->getSetting('show_summary')) {
$summary[] = $this->t('Summary field will always be visible');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$display_summary = $items[$delta]->summary || $this->getFieldSetting('display_summary');
$required = empty($form['#type']) && $this->getFieldSetting('required_summary');
$element['summary'] = [
'#type' => $display_summary ? 'textarea' : 'value',
'#default_value' => $items[$delta]->summary,
'#title' => $this->t('Summary'),
'#rows' => $this->getSetting('summary_rows'),
'#description' => !$required ? $this->t('Leave blank to use trimmed value of full text as the summary.') : '',
'#attributes' => ['class' => ['text-summary']],
'#prefix' => '<div class="js-text-summary-wrapper text-summary-wrapper">',
'#suffix' => '</div>',
'#weight' => -10,
'#required' => $required,
];
if (!$this->getSetting('show_summary') && !$required) {
$element['summary']['#attributes']['class'][] = 'js-text-summary';
$element['summary']['#attached']['library'][] = 'text/drupal.text';
}
return $element;
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
$element = parent::errorElement($element, $violation, $form, $form_state);
$property_path_array = explode('.', $violation->getPropertyPath());
return ($element === FALSE) ? FALSE : $element[$property_path_array[1]];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\text\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Plugin implementation of the 'text_textfield' widget.
*/
#[FieldWidget(
id: 'text_textfield',
label: new TranslatableMarkup('Text field'),
field_types: ['text'],
)]
class TextfieldWidget extends StringTextfieldWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$main_widget = parent::formElement($items, $delta, $element, $form, $form_state);
$allowed_formats = $this->getFieldSetting('allowed_formats');
$element = $main_widget['value'];
$element['#type'] = 'text_format';
$element['#format'] = $items[$delta]->format ?? NULL;
$element['#base_type'] = $main_widget['value']['#type'];
if ($allowed_formats && !$this->isDefaultValueWidget($form_state)) {
$element['#allowed_formats'] = $allowed_formats;
}
return $element;
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
if (isset($element['format']['#access']) && !$element['format']['#access'] && preg_match('/^[0-9]*\.format$/', $violation->getPropertyPath())) {
// Ignore validation errors for formats that may not be changed,
// such as when existing formats become invalid.
// See \Drupal\filter\Element\TextFormat::processFormat().
return FALSE;
}
return $element;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Drupal\text\Plugin\migrate\field\d6;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
// cspell:ignore optionwidgets
/**
* MigrateField Plugin for Drupal 6 text fields.
*/
#[MigrateField(
id: 'd6_text',
core: [6],
type_map: [
'text' => 'text',
'text_long' => 'text_long',
'text_with_summary' => 'text_with_summary',
],
source_module: 'text',
destination_module: 'text',
)]
class TextField extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
'text_textfield' => 'text_textfield',
];
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'default' => 'text_default',
'trimmed' => 'text_trimmed',
'plain' => 'basic_string',
];
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $field_info) {
$widget_type = $field_info['widget_type'] ?? $field_info['widget']['type'];
if ($widget_type == 'optionwidgets_onoff') {
$process = [
'value' => [
'plugin' => 'static_map',
'source' => 'value',
'default_value' => 0,
],
];
$checked_value = explode("\n", $field_info['global_settings']['allowed_values'])[1];
if (str_contains($checked_value, '|')) {
$checked_value = substr($checked_value, 0, strpos($checked_value, '|'));
}
$process['value']['map'][$checked_value] = 1;
}
else {
// See \Drupal\migrate_drupal\Plugin\migrate\source\d6\User::baseFields(),
// signature_format for an example of the YAML that represents this
// process array.
$process = [
'value' => 'value',
'format' => [
[
'plugin' => 'static_map',
'bypass' => TRUE,
'source' => 'format',
'map' => [0 => NULL],
],
[
'plugin' => 'skip_on_empty',
'method' => 'process',
],
[
'plugin' => 'migration_lookup',
'migration' => [
'd6_filter_format',
'd7_filter_format',
],
'source' => 'format',
],
],
];
}
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => $process,
];
$migration->setProcessOfProperty($field_name, $process);
}
/**
* {@inheritdoc}
*/
public function getFieldType(Row $row) {
$widget_type = $row->getSourceProperty('widget_type');
$settings = $row->getSourceProperty('global_settings');
if ($widget_type == 'text_textfield') {
$field_type = $settings['text_processing'] ? 'text' : 'string';
if (empty($settings['max_length']) || $settings['max_length'] > 255) {
$field_type .= '_long';
}
return $field_type;
}
if ($widget_type == 'text_textarea') {
$field_type = $settings['text_processing'] ? 'text_long' : 'string_long';
return $field_type;
}
switch ($widget_type) {
case 'optionwidgets_buttons':
case 'optionwidgets_select':
return 'list_string';
case 'optionwidgets_onoff':
return 'boolean';
default:
return parent::getFieldType($row);
}
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\text\Plugin\migrate\field\d7;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate_drupal\Attribute\MigrateField;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
#[MigrateField(
id: 'd7_text',
core: [7],
type_map: [
'text' => 'text',
'text_long' => 'text_long',
'text_with_summary' => 'text_with_summary',
],
source_module: 'text',
destination_module: 'text',
)]
class TextField extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldFormatterType(Row $row) {
$field_type = $this->getFieldType($row);
$formatter_type = $row->getSourceProperty('formatter/type');
switch ($field_type) {
case 'string':
$formatter_type = str_replace(['text_default', 'text_plain'], 'string', $formatter_type);
break;
case 'string_long':
$formatter_type = str_replace(['text_default', 'text_plain'], 'basic_string', $formatter_type);
break;
}
return $formatter_type;
}
/**
* {@inheritdoc}
*/
public function getFieldWidgetType(Row $row) {
$field_type = $this->getFieldType($row);
$widget_type = $row->getSourceProperty('widget/type');
switch ($field_type) {
case 'string':
$widget_type = str_replace('text_textfield', 'string_textfield', $widget_type);
break;
case 'string_long':
$widget_type = str_replace('text_textarea', 'string_textarea', $widget_type);
break;
}
return $widget_type;
}
/**
* {@inheritdoc}
*/
public function getFieldType(Row $row) {
$type = $row->getSourceProperty('type');
$plain_text = FALSE;
$filtered_text = FALSE;
foreach ($row->getSourceProperty('instances') as $instance) {
// Check if this field has plain text instances, filtered text instances,
// or both.
$data = unserialize($instance['data']);
switch ($data['settings']['text_processing']) {
case '0':
$plain_text = TRUE;
break;
case '1':
$filtered_text = TRUE;
break;
}
}
if (in_array($type, ['text', 'text_long'])) {
// If a text or text_long field has only plain text instances, migrate it
// to a string or string_long field.
if ($plain_text && !$filtered_text) {
$type = str_replace(['text', 'text_long'], ['string', 'string_long'], $type);
}
// If a text or text_long field has both plain text and filtered text
// instances, skip the row.
elseif ($plain_text && $filtered_text) {
$field_name = $row->getSourceProperty('field_name');
throw new MigrateSkipRowException("Can't migrate source field $field_name configured with both plain text and filtered text processing. See https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#plain-text");
}
}
elseif ($type == 'text_with_summary' && $plain_text) {
// If a text_with_summary field has plain text instances, skip the row
// since there's no such thing as a string_with_summary field.
$field_name = $row->getSourceProperty('field_name');
throw new MigrateSkipRowException("Can't migrate source field $field_name of type text_with_summary configured with plain text processing. See https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#plain-text");
}
return $type;
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Drupal\text;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedData;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Render\FilteredMarkup;
/**
* A computed property for processing text with a format.
*
* Required settings (below the definition's 'settings' key) are:
* - text source: The text property containing the to be processed text.
*/
class TextProcessed extends TypedData implements CacheableDependencyInterface {
/**
* Cached processed text.
*
* @var \Drupal\filter\FilterProcessResult|null
*/
protected $processed = NULL;
/**
* {@inheritdoc}
*/
public function __construct(DataDefinitionInterface $definition, $name = NULL, ?TypedDataInterface $parent = NULL) {
parent::__construct($definition, $name, $parent);
if ($definition->getSetting('text source') === NULL) {
throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed.");
}
}
/**
* {@inheritdoc}
*/
public function getValue() {
if ($this->processed !== NULL) {
return FilteredMarkup::create($this->processed->getProcessedText());
}
$item = $this->getParent();
$text = $item->{($this->definition->getSetting('text source'))};
// Avoid doing unnecessary work on empty strings.
if (!isset($text) || $text === '') {
$this->processed = new FilterProcessResult('');
}
else {
$build = [
'#type' => 'processed_text',
'#text' => $text,
'#format' => $item->format,
'#filter_types_to_skip' => [],
'#langcode' => $item->getLangcode(),
];
// Capture the cacheability metadata associated with the processed text.
$processed_text = $this->getRenderer()->renderInIsolation($build);
$this->processed = FilterProcessResult::createFromRenderArray($build)->setProcessedText((string) $processed_text);
}
return FilteredMarkup::create($this->processed->getProcessedText());
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
$this->processed = $value;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$this->getValue();
return $this->processed->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$this->getValue();
return $this->processed->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$this->getValue();
return $this->processed->getCacheMaxAge();
}
/**
* Returns the renderer service.
*
* @return \Drupal\Core\Render\RendererInterface
*/
protected function getRenderer() {
return \Drupal::service('renderer');
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for text.
*
* @group text
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,410 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Functional;
use Drupal\Component\Utility\Html;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\filter\Render\FilteredMarkup;
use Drupal\Tests\field\Functional\FunctionalString\StringFieldTest;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests the creation of text fields.
*
* @group text
* @group #slow
*/
class TextFieldTest extends StringFieldTest {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* {@inheritdoc}
*/
protected static $modules = ['entity_test', 'file', 'field_ui'];
/**
* A user with relevant administrative privileges.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer filters',
'administer entity_test fields',
]);
}
// Test fields.
/**
* Tests text field validation.
*/
public function testTextFieldValidation(): void {
// Create a field with settings to validate.
$max_length = 3;
$field_name = $this->randomMachineName();
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
'settings' => [
'max_length' => $max_length,
],
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
])->save();
// Test validation with valid and invalid values.
$entity = EntityTest::create();
for ($i = 0; $i <= $max_length + 2; $i++) {
$entity->{$field_name}->value = str_repeat('x', $i);
$violations = $entity->{$field_name}->validate();
if ($i <= $max_length) {
$this->assertCount(0, $violations, "Length $i does not cause validation error when max_length is $max_length");
}
else {
$this->assertCount(1, $violations, "Length $i causes validation error when max_length is $max_length");
}
}
}
/**
* Tests required long text with file upload.
*/
public function testRequiredLongTextWithFileUpload(): void {
// Create a text field.
$text_field_name = 'text_long';
$field_storage = FieldStorageConfig::create([
'field_name' => $text_field_name,
'entity_type' => 'entity_test',
'type' => 'text_with_summary',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
'label' => $this->randomMachineName() . '_label',
'required' => TRUE,
])->save();
// Create a file field.
$file_field_name = 'file_field';
$field_storage = FieldStorageConfig::create([
'field_name' => $file_field_name,
'entity_type' => 'entity_test',
'type' => 'file',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
'label' => $this->randomMachineName() . '_label',
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('entity_test', 'entity_test')
->setComponent($text_field_name, [
'type' => 'text_textarea_with_summary',
])
->setComponent($file_field_name, [
'type' => 'file_generic',
])
->save();
$display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
->setComponent($text_field_name)
->setComponent($file_field_name)
->save();
$test_file = current($this->drupalGetTestFiles('text'));
$edit['files[file_field_0]'] = \Drupal::service('file_system')->realpath($test_file->uri);
$this->drupalGet('entity_test/add');
$this->submitForm($edit, 'Upload');
$this->assertSession()->statusCodeEquals(200);
$edit = [
'text_long[0][value]' => 'Long text',
];
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('entity_test/1');
$this->assertSession()->pageTextContains('Long text');
}
/**
* Tests widgets.
*/
public function testTextfieldWidgets(): void {
$this->_testTextfieldWidgets('text', 'text_textfield');
$this->_testTextfieldWidgets('text_long', 'text_textarea');
}
/**
* Tests widgets + 'formatted_text' setting.
*/
public function testTextfieldWidgetsFormatted(): void {
$this->_testTextfieldWidgetsFormatted('text', 'text_textfield');
$this->_testTextfieldWidgetsFormatted('text_long', 'text_textarea');
}
/**
* Test widgets for fields with selected allowed formats.
*/
public function testTextfieldWidgetsAllowedFormats(): void {
// Create one text format.
$this->drupalLogin($this->adminUser);
$format1 = FilterFormat::create([
'format' => $this->randomMachineName(),
'name' => '1_' . $this->randomMachineName(),
]);
$format1->save();
// Create a second text format.
$format2 = FilterFormat::create([
'format' => $this->randomMachineName(),
'name' => '2_' . $this->randomMachineName(),
'filters' => [
'filter_html' => [
'status' => 1,
'settings' => [
'allowed_html' => '<strong>',
],
],
],
]);
$format2->save();
// Create a third text format.
$format3 = FilterFormat::create([
'format' => $this->randomMachineName(),
'name' => '3_' . $this->randomMachineName(),
]);
$format3->save();
// Grant access to both formats to the user.
$roles = $this->webUser->getRoles();
$rid = $roles[0];
user_role_grant_permissions($rid, [
$format1->getPermissionName(),
$format2->getPermissionName(),
$format3->getPermissionName(),
]);
// Create a field with multiple formats allowed.
$field_name = $this->randomMachineName();
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
'label' => $this->randomMachineName() . '_label',
'settings' => ['allowed_formats' => []],
]);
$field->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository */
$entity_display_repository = $this->container->get('entity_display.repository');
$entity_display_repository->getFormDisplay('entity_test', 'entity_test', 'default')
->setComponent($field_name, [
'type' => 'text_textfield',
])
->save();
$entity_display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
->setComponent($field_name)
->save();
// Display the creation form.
$this->drupalLogin($this->webUser);
$this->drupalGet('entity_test/add');
$this->assertSession()->fieldExists("{$field_name}[0][value]", NULL);
$this->assertSession()->optionExists("{$field_name}[0][format]", $format1->id());
$this->assertSession()->optionExists("{$field_name}[0][format]", $format2->id());
$this->assertSession()->optionExists("{$field_name}[0][format]", $format3->id());
$filtered_markup = FilteredMarkup::create('<div><strong><span>Hello World</span></strong></div>');
$edit = [
"{$field_name}[0][value]" => $filtered_markup,
];
$this->submitForm($edit, 'Save');
preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
$id = $match[1];
$this->assertSession()->pageTextContains('entity_test ' . $id . ' has been created.');
// Display the entity.
$entity = EntityTest::load($id);
$display = $entity_display_repository->getViewDisplay($entity->getEntityTypeId(), $entity->bundle(), 'full');
$content = $display->build($entity);
$rendered_entity = \Drupal::service('renderer')->renderRoot($content);
$this->assertStringContainsString('<div><strong><span>', (string) $rendered_entity);
// Log back in as admin.
$this->drupalLogin($this->adminUser);
// Change field to allow only one format.
$path = "entity_test/structure/entity_test/fields/entity_test.entity_test.$field_name";
$this->drupalGet($path);
$this->submitForm(["settings[allowed_formats][{$format1->id()}]" => $format1->id()], 'Save settings');
$this->drupalGet($path);
// Display the creation form.
$this->drupalLogin($this->webUser);
// We shouldn't have the 'format' selector since only one format is allowed.
$this->drupalGet('entity_test/add');
$this->assertSession()->fieldExists("{$field_name}[0][value]", NULL);
$this->assertSession()->fieldNotExists("{$field_name}[0][format]");
// Retest the entity renders fine even though filter2 is disabled.
$entity = EntityTest::load($id);
$display = $entity_display_repository->getViewDisplay($entity->getEntityTypeId(), $entity->bundle(), 'full');
$content = $display->build($entity);
$rendered_entity = \Drupal::service('renderer')->renderRoot($content);
$this->assertStringContainsString('<div><strong><span>', (string) $rendered_entity);
// Test when 2 of 3 formats are selected.
$field->setSetting('allowed_formats', [$format1->id(), $format2->id()]);
$field->save();
$this->drupalGet('entity_test/add');
// We should see the 'format' selector again.
$this->assertSession()->fieldExists("{$field_name}[0][value]", NULL);
$this->assertSession()->optionExists("{$field_name}[0][format]", $format1->id());
$this->assertSession()->optionExists("{$field_name}[0][format]", $format2->id());
$this->assertSession()->optionNotExists("{$field_name}[0][format]", $format3->id());
// Change field to allow all formats by configuring none as allowed.
$field->setSetting('allowed_formats', []);
$field->save();
$this->drupalGet('entity_test/add');
// We should see the 'format' selector again.
$this->assertSession()->fieldExists("{$field_name}[0][value]", NULL);
$this->assertSession()->optionExists("{$field_name}[0][format]", $format1->id());
$this->assertSession()->optionExists("{$field_name}[0][format]", $format2->id());
$this->assertSession()->optionExists("{$field_name}[0][format]", $format3->id());
}
/**
* Helper function for testTextfieldWidgetsFormatted().
*/
public function _testTextfieldWidgetsFormatted($field_type, $widget_type) {
// Create a field.
$field_name = $this->randomMachineName();
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => $field_type,
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
'label' => $this->randomMachineName() . '_label',
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('entity_test', 'entity_test')
->setComponent($field_name, [
'type' => $widget_type,
])
->save();
$display_repository->getViewDisplay('entity_test', 'entity_test', 'full')
->setComponent($field_name)
->save();
// Disable all text formats besides the plain text fallback format.
$this->drupalLogin($this->adminUser);
foreach (filter_formats() as $format) {
if (!$format->isFallbackFormat()) {
$this->drupalGet('admin/config/content/formats/manage/' . $format->id() . '/disable');
$this->submitForm([], 'Disable');
}
}
$this->drupalLogin($this->webUser);
// Display the creation form. Since the user only has access to one format,
// no format selector will be displayed.
$this->drupalGet('entity_test/add');
$this->assertSession()->fieldValueEquals("{$field_name}[0][value]", '');
$this->assertSession()->fieldNotExists("{$field_name}[0][format]");
// Submit with data that should be filtered.
$value = '<em>' . $this->randomMachineName() . '</em>';
$edit = [
"{$field_name}[0][value]" => $value,
];
$this->submitForm($edit, 'Save');
preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
$id = $match[1];
$this->assertSession()->pageTextContains('entity_test ' . $id . ' has been created.');
// Display the entity.
$entity = EntityTest::load($id);
$display = $display_repository->getViewDisplay($entity->getEntityTypeId(), $entity->bundle(), 'full');
$content = $display->build($entity);
$rendered_entity = \Drupal::service('renderer')->renderRoot($content);
$this->assertStringNotContainsString($value, (string) $rendered_entity);
$this->assertStringContainsString(Html::escape($value), (string) $rendered_entity);
// Create a new text format that does not escape HTML, and grant the user
// access to it.
$this->drupalLogin($this->adminUser);
$edit = [
'format' => $this->randomMachineName(),
'name' => $this->randomMachineName(),
];
$this->drupalGet('admin/config/content/formats/add');
$this->submitForm($edit, 'Save configuration');
filter_formats_reset();
$format = FilterFormat::load($edit['format']);
$format_id = $format->id();
$permission = $format->getPermissionName();
$roles = $this->webUser->getRoles();
$rid = $roles[0];
user_role_grant_permissions($rid, [$permission]);
$this->drupalLogin($this->webUser);
// Display edition form.
// We should now have a 'text format' selector.
$this->drupalGet('entity_test/manage/' . $id . '/edit');
$this->assertSession()->fieldExists("{$field_name}[0][value]");
$this->assertSession()->fieldExists("{$field_name}[0][format]");
// Edit and change the text format to the new one that was created.
$edit = [
"{$field_name}[0][format]" => $format_id,
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('entity_test ' . $id . ' has been updated.');
// Display the entity.
$this->container->get('entity_type.manager')->getStorage('entity_test')->resetCache([$id]);
$entity = EntityTest::load($id);
$display = $display_repository->getViewDisplay($entity->getEntityTypeId(), $entity->bundle(), 'full');
$content = $display->build($entity);
$rendered_entity = \Drupal::service('renderer')->renderRoot($content);
$this->assertStringContainsString($value, (string) $rendered_entity);
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JavaScript functionality of the text_textarea_with_summary widget.
*
* @group text
*/
class TextareaWithSummaryTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['text', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page']);
$account = $this->drupalCreateUser([
'create page content',
'edit own page content',
]);
$this->drupalLogin($account);
}
/**
* Helper to test toggling the summary area.
*
* @internal
*/
protected function assertSummaryToggle(): void {
$this->drupalGet('node/add/page');
$widget = $this->getSession()->getPage()->findById('edit-body-wrapper');
$summary_field = $widget->findField('edit-body-0-summary');
$this->assertEquals(FALSE, $summary_field->isVisible(), 'Summary field is hidden by default.');
$this->assertEquals(FALSE, $widget->hasButton('Hide summary'), 'No Hide summary link by default.');
$widget->pressButton('Edit summary');
$this->assertEquals(FALSE, $widget->hasButton('Edit summary'), 'Edit summary link is removed after clicking.');
$this->assertEquals(TRUE, $summary_field->isVisible(), 'Summary field is shown.');
$widget->pressButton('Hide summary');
$this->assertEquals(FALSE, $widget->hasButton('Hide summary'), 'Hide summary link is removed after clicking.');
$this->assertEquals(FALSE, $summary_field->isVisible(), 'Summary field is hidden again.');
$this->assertEquals(TRUE, $widget->hasButton('Edit summary'), 'Edit summary link is visible again.');
}
/**
* Tests the textSummary javascript behavior.
*/
public function testTextSummaryBehavior(): void {
// Test with field defaults.
$this->assertSummaryToggle();
// Repeat test with non-empty field description.
$body_field = FieldConfig::loadByName('node', 'page', 'body');
$body_field->set('description', 'Text with Summary field description.');
$body_field->save();
$this->assertSummaryToggle();
// Repeat test with unlimited cardinality field.
$body_field_storage = FieldStorageConfig::loadByName('node', 'body');
$body_field_storage->setCardinality(-1);
$body_field_storage->save();
$this->assertSummaryToggle();
// Test summary is shown when non-empty.
$node = $this->createNode([
'body' => [
[
'value' => $this->randomMachineName(32),
'summary' => $this->randomMachineName(32),
'format' => filter_default_format(),
],
],
]);
$this->drupalGet('node/' . $node->id() . '/edit');
$summary_field = $this->getSession()->getPage()->findField('edit-body-0-summary');
$this->assertEquals(TRUE, $summary_field->isVisible());
}
/**
* Tests that the textSummary behavior is not run for required summary fields.
*/
public function testTextSummaryRequiredBehavior(): void {
// Test with field defaults.
$this->assertSummaryToggle();
// Create a second field with a required summary.
$field_name = $this->randomMachineName();
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'text_with_summary',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
'label' => $this->randomMachineName() . '_label',
'settings' => [
'display_summary' => TRUE,
'required_summary' => TRUE,
],
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('node', 'page')
->setComponent($field_name, [
'type' => 'text_textarea_with_summary',
])
->save();
$this->drupalGet('node/add/page');
$page = $this->getSession()->getPage();
$summary_field = $page->findField('edit-' . $field_name . '-0-summary');
$this->assertEquals(TRUE, $summary_field->isVisible());
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Kernel\Migrate;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to text.settings.yml.
*
* @group migrate_drupal_6
*/
class MigrateTextConfigsTest extends MigrateDrupal6TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('text_settings');
}
/**
* Tests migration of text variables to text.settings.yml.
*/
public function testTextSettings(): void {
$config = $this->config('text.settings');
$this->assertSame(456, $config->get('default_summary_length'));
$this->assertConfigSchema(\Drupal::service('config.typed'), 'text.settings', $config->get());
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the text formatters functionality.
*
* @group text
*/
class TextFormatterTest extends EntityKernelTestBase {
/**
* The entity type used in this test.
*
* @var string
*/
protected $entityType = 'entity_test';
/**
* The bundle used in this test.
*
* @var string
*/
protected $bundle = 'entity_test';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['text'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
FilterFormat::create([
'format' => 'my_text_format',
'name' => 'My text format',
'filters' => [
'filter_autop' => [
'module' => 'filter',
'status' => TRUE,
],
],
])->save();
FieldStorageConfig::create([
'field_name' => 'formatted_text',
'entity_type' => $this->entityType,
'type' => 'text',
'settings' => [],
])->save();
FieldConfig::create([
'entity_type' => $this->entityType,
'bundle' => $this->bundle,
'field_name' => 'formatted_text',
'label' => 'Filtered text',
])->save();
}
/**
* Tests all text field formatters.
*/
public function testFormatters(): void {
$formatters = [
'text_default',
'text_trimmed',
'text_summary_or_trimmed',
];
// Create the entity to be referenced.
$entity = $this->container->get('entity_type.manager')
->getStorage($this->entityType)
->create(['name' => $this->randomMachineName()]);
$entity->formatted_text = [
'value' => 'Hello, world!',
'format' => 'my_text_format',
];
$entity->save();
foreach ($formatters as $formatter) {
// Verify the text field formatter's render array.
$build = $entity->get('formatted_text')->view(['type' => $formatter]);
\Drupal::service('renderer')->renderRoot($build[0]);
$this->assertSame("<p>Hello, world!</p>\n", (string) $build[0]['#markup']);
$this->assertEquals(FilterFormat::load('my_text_format')->getCacheTags(), $build[0]['#cache']['tags'], "The $formatter formatter has the expected cache tags when formatting a formatted text field.");
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Kernel;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\KernelTests\KernelTestBase;
use Drupal\text\Plugin\Field\FieldType\TextItemBase;
/**
* Tests TextItemBase.
*
* @coversDefaultClass \Drupal\text\Plugin\Field\FieldType\TextItemBase
* @group text
*/
class TextItemBaseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['filter', 'text', 'entity_test', 'field'];
/**
* Tests creation of sample values.
*
* @covers ::generateSampleValue
* @dataProvider providerTextFieldSampleValue
*/
public function testTextFieldSampleValue($max_length): void {
// Create a text field.
$field_definition = BaseFieldDefinition::create('text')
->setTargetEntityTypeId('foo');
// Ensure testing of max_lengths from 1 to 3 because generateSampleValue
// creates a sentence with a maximum number of words set to 1/3 of the
// max_length of the field.
$field_definition->setSetting('max_length', $max_length);
$sample_value = TextItemBase::generateSampleValue($field_definition);
$this->assertEquals($max_length, strlen($sample_value['value']));
}
/**
* Data provider for testTextFieldSampleValue.
*/
public static function providerTextFieldSampleValue() {
return [
[
1,
],
[
2,
],
[
3,
],
[
4,
],
];
}
/**
* @covers ::calculateDependencies
*/
public function testCalculateDependencies(): void {
$format = FilterFormat::create([
'format' => 'test_format',
'name' => 'Test format',
]);
$format->save();
$fieldName = $this->randomMachineName();
$field_storage = FieldStorageConfig::create([
'field_name' => $fieldName,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => $fieldName,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'settings' => [
'allowed_formats' => [$format->id()],
],
]);
$field->save();
$field->calculateDependencies();
$this->assertEquals([
'module' => [
'entity_test',
'text',
],
'config' => [
"field.storage.entity_test.$fieldName",
'filter.format.test_format',
],
], $field->getDependencies());
}
}

View File

@@ -0,0 +1,360 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Kernel;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Render\FilteredMarkup;
use Drupal\KernelTests\KernelTestBase;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests text_summary() with different strings and lengths.
*
* @group text
*/
class TextSummaryTest extends KernelTestBase {
use UserCreationTrait;
protected static $modules = [
'system',
'user',
'filter',
'text',
'field',
'entity_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['text']);
}
/**
* Tests text summaries for a question followed by a sentence.
*/
public function testFirstSentenceQuestion(): void {
$text = 'A question? A sentence. Another sentence.';
$expected = 'A question? A sentence.';
$this->assertTextSummary($text, $expected, NULL, 30);
}
/**
* Tests summary with long example.
*/
public function testLongSentence(): void {
// 125.
// cSpell:disable
$text =
'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' .
// 108.
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' .
// 103.
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' .
// 110.
'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
$expected = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' .
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' .
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.';
// cSpell:enable
// First three sentences add up to: 336, so add one for space and then 3 to get half-way into next word.
$this->assertTextSummary($text, $expected, NULL, 340);
}
/**
* Tests various summary length edge cases.
*/
public function testLength(): void {
FilterFormat::create([
'format' => 'autop',
'name' => 'Autop',
'filters' => [
'filter_autop' => [
'status' => 1,
],
],
])->save();
FilterFormat::create([
'format' => 'autop_correct',
'name' => 'Autop correct',
'filters' => [
'filter_autop' => [
'status' => 1,
],
'filter_htmlcorrector' => [
'status' => 1,
],
],
])->save();
// This string tests a number of edge cases.
$text = "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>";
// The summaries we expect text_summary() to return when $size is the index
// of each array item.
// Using no text format:
$format = NULL;
$i = 0;
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<", $format, $i++);
$this->assertTextSummary($text, "<p", $format, $i++);
$this->assertTextSummary($text, "<p>", $format, $i++);
$this->assertTextSummary($text, "<p>\n", $format, $i++);
$this->assertTextSummary($text, "<p>\nH", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n<", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
// Using a text format with filter_autop enabled.
$format = 'autop';
$i = 0;
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<", $format, $i++);
$this->assertTextSummary($text, "<p", $format, $i++);
$this->assertTextSummary($text, "<p>", $format, $i++);
$this->assertTextSummary($text, "<p>", $format, $i++);
$this->assertTextSummary($text, "<p>", $format, $i++);
$this->assertTextSummary($text, "<p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
// Using a text format with filter_autop and filter_htmlcorrector enabled.
$format = 'autop_correct';
$i = 0;
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "", $format, $i++);
$this->assertTextSummary($text, "<p></p>", $format, $i++);
$this->assertTextSummary($text, "<p></p>", $format, $i++);
$this->assertTextSummary($text, "<p></p>", $format, $i++);
$this->assertTextSummary($text, "<p></p>", $format, $i++);
$this->assertTextSummary($text, "<p></p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
$this->assertTextSummary($text, "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>", $format, $i++);
}
/**
* Tests text summaries with an invalid filter format.
*
* @see text_summary()
*/
public function testInvalidFilterFormat(): void {
$this->assertTextSummary($this->randomString(100), '', 'non_existent_format');
}
/**
* Calls text_summary() and asserts that the expected teaser is returned.
*
* @internal
*/
public function assertTextSummary(string $text, string $expected, ?string $format = NULL, ?int $size = NULL): void {
$summary = text_summary($text, $format, $size);
$this->assertSame($expected, $summary, '<pre style="white-space: pre-wrap">' . $summary . '</pre> is identical to <pre style="white-space: pre-wrap">' . $expected . '</pre>');
}
/**
* Tests required summary.
*/
public function testRequiredSummary(): void {
$this->installEntitySchema('entity_test');
$this->setUpCurrentUser();
$field_definition = FieldStorageConfig::create([
'field_name' => 'test_text_with_summary',
'type' => 'text_with_summary',
'entity_type' => 'entity_test',
'cardinality' => 1,
'settings' => [
'max_length' => 200,
],
]);
$field_definition->save();
$instance = FieldConfig::create([
'field_name' => 'test_text_with_summary',
'label' => 'A text field',
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'settings' => [
'text_processing' => TRUE,
'display_summary' => TRUE,
'required_summary' => TRUE,
],
]);
$instance->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
])->setComponent('test_text_with_summary', [
'type' => 'text_textarea_with_summary',
'settings' => [
'summary_rows' => 2,
'show_summary' => TRUE,
],
])
->save();
// Check the required summary.
$entity = EntityTest::create([
'name' => $this->randomMachineName(),
'type' => 'entity_test',
'test_text_with_summary' => ['value' => $this->randomMachineName()],
]);
$form = \Drupal::service('entity.form_builder')->getForm($entity);
$this->assertNotEmpty($form['test_text_with_summary']['widget'][0]['summary'], 'Summary field is shown');
$this->assertNotEmpty($form['test_text_with_summary']['widget'][0]['summary']['#required'], 'Summary field is required');
// Test validation.
/** @var \Symfony\Component\Validator\ConstraintViolation[] $violations */
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertEquals('test_text_with_summary.0.summary', $violations[0]->getPropertyPath());
$this->assertEquals('The summary field is required for A text field', $violations[0]->getMessage());
}
/**
* Test text normalization when filter_html or filter_htmlcorrector enabled.
*/
public function testNormalization(): void {
FilterFormat::create([
'format' => 'filter_html_enabled',
'name' => 'Filter HTML enabled',
'filters' => [
'filter_html' => [
'status' => 1,
'settings' => [
'allowed_html' => '<strong>',
],
],
],
])->save();
FilterFormat::create([
'format' => 'filter_htmlcorrector_enabled',
'name' => 'Filter HTML corrector enabled',
'filters' => [
'filter_htmlcorrector' => [
'status' => 1,
],
],
])->save();
FilterFormat::create([
'format' => 'neither_filter_enabled',
'name' => 'Neither filter enabled',
'filters' => [],
])->save();
$filtered_markup = FilteredMarkup::create('<div><strong><span>Hello World</span></strong></div>');
// With either HTML filter enabled, text_summary() will normalize the text
// using HTML::normalize().
$summary = text_summary($filtered_markup, 'filter_html_enabled', 30);
$this->assertStringContainsString('<div><strong><span>', $summary);
$this->assertStringContainsString('</span></strong></div>', $summary);
$summary = text_summary($filtered_markup, 'filter_htmlcorrector_enabled', 30);
$this->assertStringContainsString('<div><strong><span>', $summary);
$this->assertStringContainsString('</span></strong></div>', $summary);
// If neither filter is enabled, the text will not be normalized.
$summary = text_summary($filtered_markup, 'neither_filter_enabled', 30);
$this->assertStringContainsString('<div><strong><span>', $summary);
$this->assertStringNotContainsString('</span></strong></div>', $summary);
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Kernel;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\field\Kernel\FieldKernelTestBase;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests using entity fields of the text summary field type.
*
* @group text
*/
class TextWithSummaryItemTest extends FieldKernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['filter'];
/**
* Field storage entity.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $fieldStorage;
/**
* Field entity.
*
* @var \Drupal\field\Entity\FieldConfig
*/
protected $field;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test_rev');
// Create the necessary formats.
$this->installConfig(['filter']);
FilterFormat::create([
'format' => 'no_filters',
'name' => 'No filters',
'filters' => [],
])->save();
}
/**
* Tests processed properties.
*/
public function testCrudAndUpdate(): void {
$entity_type = 'entity_test';
$this->createField($entity_type);
// Create an entity with a summary and no text format.
$storage = $this->container->get('entity_type.manager')
->getStorage($entity_type);
$entity = $storage->create();
$entity->summary_field->value = $value = $this->randomMachineName();
$entity->summary_field->summary = $summary = $this->randomMachineName();
$entity->summary_field->format = NULL;
$entity->name->value = $this->randomMachineName();
$entity->save();
$entity = $storage->load($entity->id());
$this->assertInstanceOf(FieldItemListInterface::class, $entity->summary_field);
$this->assertInstanceOf(FieldItemInterface::class, $entity->summary_field[0]);
$this->assertEquals($value, $entity->summary_field->value);
$this->assertEquals($summary, $entity->summary_field->summary);
$this->assertNull($entity->summary_field->format);
// Even if no format is given, if text processing is enabled, the default
// format is used.
$this->assertSame("<p>{$value}</p>\n", (string) $entity->summary_field->processed);
$this->assertSame("<p>{$summary}</p>\n", (string) $entity->summary_field->summary_processed);
// Change the format, this should update the processed properties.
$entity->summary_field->format = 'no_filters';
$this->assertSame($value, (string) $entity->summary_field->processed);
$this->assertSame($summary, (string) $entity->summary_field->summary_processed);
// Test the generateSampleValue() method.
$entity = $this->container->get('entity_type.manager')
->getStorage($entity_type)
->create();
$entity->summary_field->generateSampleItems();
$this->entityValidateAndSave($entity);
}
/**
* Creates a text_with_summary field storage and field.
*
* @param string $entity_type
* Entity type for which the field should be created.
*/
protected function createField($entity_type) {
// Create a field .
$this->fieldStorage = FieldStorageConfig::create([
'field_name' => 'summary_field',
'entity_type' => $entity_type,
'type' => 'text_with_summary',
'settings' => [
'max_length' => 10,
],
]);
$this->fieldStorage->save();
$this->field = FieldConfig::create([
'field_storage' => $this->fieldStorage,
'bundle' => $entity_type,
]);
$this->field->save();
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Unit\Plugin\migrate\field\d6;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
use Drupal\text\Plugin\migrate\field\d6\TextField;
use Prophecy\Argument;
// cspell:ignore optionwidgets
/**
* @coversDefaultClass \Drupal\text\Plugin\migrate\field\d6\TextField
* @group text
*/
class TextFieldTest extends UnitTestCase {
/**
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface
*/
protected $plugin;
/**
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->plugin = new TextField([], 'text', []);
$migration = $this->prophesize(MigrationInterface::class);
// The plugin's defineValueProcessPipeline() method will call
// setProcessOfProperty() and return nothing. So, in order to examine the
// process pipeline created by the plugin, we need to ensure that
// getProcess() always returns the last input to setProcessOfProperty().
$migration->setProcessOfProperty(Argument::type('string'), Argument::type('array'))
->will(function ($arguments) use ($migration) {
$migration->getProcess()->willReturn($arguments[1]);
});
$this->migration = $migration->reveal();
}
/**
* @covers ::defineValueProcessPipeline
*/
public function testFilteredTextValueProcessPipeline(): void {
$field_info = [
'widget_type' => 'text_textfield',
];
$this->plugin->defineValueProcessPipeline($this->migration, 'body', $field_info);
$process = $this->migration->getProcess();
$this->assertSame('sub_process', $process['plugin']);
$this->assertSame('body', $process['source']);
$this->assertSame('value', $process['process']['value']);
// Ensure that filter format IDs will be looked up in the filter format
// migrations.
$lookup = $process['process']['format'][2];
$this->assertSame('migration_lookup', $lookup['plugin']);
$this->assertContains('d6_filter_format', $lookup['migration']);
$this->assertContains('d7_filter_format', $lookup['migration']);
$this->assertSame('format', $lookup['source']);
}
/**
* @covers ::defineValueProcessPipeline
*/
public function testBooleanTextImplicitValueProcessPipeline(): void {
$info = [
'widget_type' => 'optionwidgets_onoff',
'global_settings' => [
'allowed_values' => "foo\nbar",
],
];
$this->plugin->defineValueProcessPipeline($this->migration, 'field', $info);
$expected = [
'value' => [
'plugin' => 'static_map',
'source' => 'value',
'default_value' => 0,
'map' => [
'bar' => 1,
],
],
];
$this->assertSame($expected, $this->migration->getProcess()['process']);
}
/**
* @covers ::defineValueProcessPipeline
*/
public function testBooleanTextExplicitValueProcessPipeline(): void {
$info = [
'widget_type' => 'optionwidgets_onoff',
'global_settings' => [
'allowed_values' => "foo|Foo\nBaz|Baz",
],
];
$this->plugin->defineValueProcessPipeline($this->migration, 'field', $info);
$expected = [
'value' => [
'plugin' => 'static_map',
'source' => 'value',
'default_value' => 0,
'map' => [
'Baz' => 1,
],
],
];
$this->assertSame($expected, $this->migration->getProcess()['process']);
}
/**
* Data provider for testGetFieldType().
*/
public static function getFieldTypeProvider() {
return [
['string_long', 'text_textfield', ['text_processing' => FALSE]],
['string', 'text_textfield', [
'text_processing' => FALSE,
'max_length' => 128,
],
],
['string_long', 'text_textfield', [
'text_processing' => FALSE,
'max_length' => 4096,
],
],
['text_long', 'text_textfield', ['text_processing' => TRUE]],
['text', 'text_textfield', [
'text_processing' => TRUE,
'max_length' => 128,
],
],
['text_long', 'text_textfield', [
'text_processing' => TRUE,
'max_length' => 4096,
],
],
['list_string', 'optionwidgets_buttons'],
['list_string', 'optionwidgets_select'],
['boolean', 'optionwidgets_onoff'],
['text_long', 'text_textarea', ['text_processing' => TRUE]],
['string_long', 'text_textarea', ['text_processing' => FALSE]],
[NULL, 'undefined'],
];
}
/**
* @covers ::getFieldType
* @dataProvider getFieldTypeProvider
*/
public function testGetFieldType($expected_type, $widget_type, array $settings = []): void {
$row = new Row();
$row->setSourceProperty('widget_type', $widget_type);
$row->setSourceProperty('global_settings', $settings);
$this->assertSame($expected_type, $this->plugin->getFieldType($row));
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\text\Unit\Plugin\migrate\field\d7;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
use Drupal\text\Plugin\migrate\field\d7\TextField;
/**
* @coversDefaultClass \Drupal\text\Plugin\migrate\field\d7\TextField
* @group text
*/
class TextFieldTest extends UnitTestCase {
/**
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface
*/
protected $plugin;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->plugin = new TextField([], 'text', []);
}
/**
* Data provider for testGetFieldFormatterType().
*/
public static function getFieldFormatterTypeProvider() {
return [
['text', 'text_plain', 'string'],
['text_long', 'text_default', 'basic_string'],
['text_long', 'text_plain', 'basic_string'],
];
}
/**
* @covers ::getFieldFormatterType
* @covers ::getFieldType
* @dataProvider getFieldFormatterTypeProvider
*/
public function testGetFieldFormatterType($type, $formatter_type, $expected): void {
$row = new Row();
$row->setSourceProperty('type', $type);
$row->setSourceProperty('formatter/type', $formatter_type);
$row->setSourceProperty('instances', [
[
'data' => serialize([
'settings' => [
'text_processing' => '0',
],
]),
],
]);
$this->assertEquals($expected, $this->plugin->getFieldFormatterType($row));
}
}

View File

@@ -0,0 +1,6 @@
formatted_text:
label: 'Formatted text'
description: 'Text field with markup support and optional editor.'
weight: -45
libraries:
- text/drupal.text-icon

13
core/modules/text/text.info.yml Executable file
View File

@@ -0,0 +1,13 @@
name: Text
type: module
description: 'Defines field types for short and long text with optional summaries.'
package: Field types
# version: VERSION
dependencies:
- drupal:field
- drupal:filter
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

73
core/modules/text/text.js Executable file
View File

@@ -0,0 +1,73 @@
/**
* @file
* Text behaviors.
*/
(function ($, Drupal) {
/**
* Auto-hide summary textarea if empty and show hide and unhide links.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches auto-hide behavior on `text-summary` events.
*/
Drupal.behaviors.textSummary = {
attach(context, settings) {
once('text-summary', '.js-text-summary', context).forEach((summary) => {
const $widget = $(summary).closest('.js-text-format-wrapper');
const $summary = $widget.find('.js-text-summary-wrapper');
const $summaryLabel = $summary.find('label').eq(0);
const $full = $widget.children('.js-form-type-textarea');
let $fullLabel = $full.find('label').eq(0);
// Create a placeholder label when the field cardinality is greater
// than 1.
if ($fullLabel.length === 0) {
$fullLabel = $('<label></label>').prependTo($full);
}
// To ensure the summary toggle is shown in case the label is hidden
// (in multivalue fields in particular), show the label but hide
// the original text of the label.
if ($fullLabel.hasClass('visually-hidden')) {
$fullLabel.html(
(index, oldHtml) =>
`<span class="visually-hidden">${oldHtml}</span>`,
);
$fullLabel.removeClass('visually-hidden');
}
// Set up the edit/hide summary link.
const $link = $(
`<span class="field-edit-link"> (<button type="button" class="link link-edit-summary">${Drupal.t(
'Hide summary',
)}</button>)</span>`,
);
const $button = $link.find('button');
let toggleClick = true;
$link
.on('click', (e) => {
if (toggleClick) {
$summary.hide();
$button.html(Drupal.t('Edit summary'));
$link.appendTo($fullLabel);
} else {
$summary.show();
$button.html(Drupal.t('Hide summary'));
$link.appendTo($summaryLabel);
}
e.preventDefault();
toggleClick = !toggleClick;
})
.appendTo($summaryLabel);
// If no summary is set, hide the summary field.
if (summary.value === '') {
$link.trigger('click');
}
});
},
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,16 @@
drupal.text:
version: VERSION
js:
text.js: {}
dependencies:
- core/jquery
- core/once
- core/drupal
drupal.text-icon:
version: VERSION
css:
theme:
css/text.icon.theme.css: {}
dependencies:
- field_ui/drupal.field_ui.manage_fields

166
core/modules/text/text.module Executable file
View File

@@ -0,0 +1,166 @@
<?php
/**
* @file
* Defines simple text field types.
*/
use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\filter\Entity\FilterFormat;
/**
* Implements hook_help().
*/
function text_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.text':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Text module allows you to create short and long text fields with optional summaries. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":text_documentation">online documentation for the Text module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':text_documentation' => 'https://www.drupal.org/documentation/modules/text']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Managing and displaying text fields') . '</dt>';
$output .= '<dd>' . t('The <em>settings</em> and <em>display</em> of the text field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
$output .= '<dt>' . t('Creating short text fields') . '</dt>';
$output .= '<dd>' . t('If you choose <em>Text (plain)</em> or <em>Text (formatted)</em> as the field type on the <em>Manage fields</em> page, then a field with a single row is displayed. You can change the maximum text length in the <em>Field settings</em> when you set up the field.') . '</dd>';
$output .= '<dt>' . t('Creating long text fields') . '</dt>';
$output .= '<dd>' . t('If you choose <em>Text (plain, long)</em>, <em>Text (formatted, long)</em>, or <em>Text (formatted, long, with summary)</em> on the <em>Manage fields</em> page, then users can insert text of unlimited length. On the <em>Manage form display</em> page, you can set the number of rows that are displayed to users.') . '</dd>';
$output .= '<dt>' . t('Trimming the text length') . '</dt>';
$output .= '<dd>' . t('On the <em>Manage display</em> page you can choose to display a trimmed version of the text, and if so, where to cut off the text.') . '</dd>';
$output .= '<dt>' . t('Displaying summaries instead of trimmed text') . '</dt>';
$output .= '<dd>' . t('As an alternative to using a trimmed version of the text, you can enter a separate summary by choosing the <em>Text (formatted, long, with summary)</em> field type on the <em>Manage fields</em> page. Even when <em>Summary input</em> is enabled, and summaries are provided, you can display <em>trimmed</em> text nonetheless by choosing the appropriate format on the <em>Manage display</em> page.') . '</dd>';
$output .= '<dt>' . t('Using text formats and editors') . '</dt>';
$output .= '<dd>' . t('If you choose <em>Text (plain)</em> or <em>Text (plain, long)</em> you restrict the input to <em>Plain text</em> only. If you choose <em>Text (formatted)</em>, <em>Text (formatted, long)</em>, or <em>Text (formatted, long with summary)</em> you allow users to write formatted text. Which options are available to individual users depends on the settings on the <a href=":formats">Text formats and editors page</a>.', [':formats' => Url::fromRoute('filter.admin_overview')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Generates a trimmed, formatted version of a text field value.
*
* If the end of the summary is not indicated using the <!--break--> delimiter
* then we generate the summary automatically, trying to end it at a sensible
* place such as the end of a paragraph, a line break, or the end of a sentence
* (in that order of preference).
*
* @param $text
* The content for which a summary will be generated.
* @param $format
* The format of the content. If the line break filter is present then we
* treat newlines embedded in $text as line breaks. If the htmlcorrector
* filter is present, it will be run on the generated summary (if different
* from the incoming $text).
* @param $size
* The desired character length of the summary. If omitted, the default value
* will be used. Ignored if the special delimiter is present in $text.
*
* @return string
* The generated summary.
*/
function text_summary($text, $format = NULL, $size = NULL) {
if (!isset($size)) {
$size = \Drupal::config('text.settings')->get('default_summary_length');
}
// Find where the delimiter is in the body.
$delimiter = strpos($text, '<!--break-->');
// If the size is zero, and there is no delimiter, the entire body is the summary.
if ($size == 0 && $delimiter === FALSE) {
return $text;
}
// If a valid delimiter has been specified, use it to chop off the summary.
if ($delimiter !== FALSE) {
return substr($text, 0, $delimiter);
}
// Retrieve the filters of the specified text format, if any.
if (isset($format)) {
$filter_format = FilterFormat::load($format);
// If the specified format does not exist, return nothing. $text is already
// filtered text, but the remainder of this function will not be able to
// ensure a sane and secure summary.
if (!$filter_format || !($filters = $filter_format->filters())) {
return '';
}
}
// If we have a short body, the entire body is the summary.
if (mb_strlen($text) <= $size) {
return $text;
}
// If the delimiter has not been specified, try to split at paragraph or
// sentence boundaries.
// The summary may not be longer than maximum length specified. Initial slice.
$summary = Unicode::truncate($text, $size);
// Store the actual length of the UTF8 string -- which might not be the same
// as $size.
$max_right_pos = strlen($summary);
// How much to cut off the end of the summary so that it doesn't end in the
// middle of a paragraph, sentence, or word.
// Initialize it to maximum in order to find the minimum.
$min_right_pos = $max_right_pos;
// Store the reverse of the summary. We use strpos on the reversed needle and
// haystack for speed and convenience.
$reversed = strrev($summary);
// Build an array of arrays of break points grouped by preference.
$break_points = [];
// A paragraph near the end of sliced summary is most preferable.
$break_points[] = ['</p>' => 0];
// If no complete paragraph then treat line breaks as paragraphs.
$line_breaks = ['<br />' => 6, '<br>' => 4];
// Newline only indicates a line break if line break converter
// filter is present.
if (isset($format) && $filters->has('filter_autop') && $filters->get('filter_autop')->status) {
$line_breaks["\n"] = 1;
}
$break_points[] = $line_breaks;
// If the first paragraph is too long, split at the end of a sentence.
$break_points[] = ['. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1];
// Iterate over the groups of break points until a break point is found.
foreach ($break_points as $points) {
// Look for each break point, starting at the end of the summary.
foreach ($points as $point => $offset) {
// The summary is already reversed, but the break point isn't.
$right_pos = strpos($reversed, strrev($point));
if ($right_pos !== FALSE) {
$min_right_pos = min($right_pos + $offset, $min_right_pos);
}
}
// If a break point was found in this group, slice and stop searching.
if ($min_right_pos !== $max_right_pos) {
// Don't slice with length 0. Length must be <0 to slice from RHS.
$summary = ($min_right_pos === 0) ? $summary : substr($summary, 0, 0 - $min_right_pos);
break;
}
}
// If filter_html or filter_htmlcorrector is enabled, normalize the output.
if (isset($format)) {
$filter_enabled = function (string $filter) use ($filters) : bool {
return $filters->has($filter) && $filters->get($filter)->status;
};
if ($filter_enabled('filter_html') || $filter_enabled('filter_htmlcorrector')) {
$summary = Html::normalize($summary);
}
}
return $summary;
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains post update hooks for the text module.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\field\FieldConfigInterface;
use Drupal\text\Plugin\Field\FieldType\TextItemBase;
/**
* Implements hook_removed_post_updates().
*/
function text_removed_post_updates() {
return [
'text_post_update_add_required_summary_flag' => '9.0.0',
'text_post_update_add_required_summary_flag_form_display' => '10.0.0',
];
}
/**
* Add allowed_formats setting to existing text fields.
*/
function text_post_update_allowed_formats(&$sandbox = NULL) {
\Drupal::classResolver(ConfigEntityUpdater::class)
->update($sandbox, 'field_config', function (FieldConfigInterface $field_config) {
$class = get_class($field_config);
// Deal only with text fields and descendants.
if (is_a($class, TextItemBase::class, TRUE)) {
// Get the existing allowed_formats setting.
$allowed_formats = $field_config->get('settings.allowed_formats');
if (!is_array($allowed_formats) && empty($allowed_formats)) {
// Save default value if existing value not present.
$field_config->set('settings.allowed_formats', []);
return TRUE;
}
}
return FALSE;
});
}