Initial Drupal 11 with DDEV setup
This commit is contained in:
1
web/core/modules/text/config/install/text.settings.yml
Normal file
1
web/core/modules/text/config/install/text.settings.yml
Normal file
@ -0,0 +1 @@
|
||||
default_summary_length: 600
|
||||
160
web/core/modules/text/config/schema/text.schema.yml
Normal file
160
web/core/modules/text/config/schema/text.schema.yml
Normal file
@ -0,0 +1,160 @@
|
||||
# Schema for the configuration files of the text module.
|
||||
|
||||
text.settings:
|
||||
type: config_object
|
||||
label: 'Text settings'
|
||||
constraints:
|
||||
FullyValidatable: ~
|
||||
mapping:
|
||||
default_summary_length:
|
||||
type: integer
|
||||
label: 'Default summary length'
|
||||
constraints:
|
||||
NotNull: [ ]
|
||||
Range:
|
||||
min: 1
|
||||
|
||||
field.storage_settings.text:
|
||||
type: mapping
|
||||
label: 'Text (formatted) settings'
|
||||
mapping:
|
||||
max_length:
|
||||
type: integer
|
||||
label: 'Maximum length'
|
||||
|
||||
field.field_settings.text:
|
||||
type: mapping
|
||||
label: 'Text (formatted) settings'
|
||||
mapping:
|
||||
allowed_formats:
|
||||
type: sequence
|
||||
label: 'Allowed text formats'
|
||||
sequence:
|
||||
type: string
|
||||
|
||||
field.value.text:
|
||||
type: mapping
|
||||
label: 'Default value'
|
||||
mapping:
|
||||
value:
|
||||
type: label
|
||||
label: 'Value'
|
||||
format:
|
||||
type: string
|
||||
label: 'Text format'
|
||||
|
||||
field.storage_settings.text_long:
|
||||
label: 'Text (formatted, long) settings'
|
||||
type: field.field_settings.text
|
||||
|
||||
field.field_settings.text_long:
|
||||
label: 'Text (formatted, long) settings'
|
||||
type: mapping
|
||||
mapping:
|
||||
allowed_formats:
|
||||
type: sequence
|
||||
label: 'Allowed text formats'
|
||||
sequence:
|
||||
type: string
|
||||
|
||||
field.value.text_long:
|
||||
type: mapping
|
||||
label: 'Default value'
|
||||
mapping:
|
||||
value:
|
||||
type: text
|
||||
label: 'Value'
|
||||
format:
|
||||
type: string
|
||||
label: 'Text format'
|
||||
|
||||
# This field type has no field storage settings, so no specific config schema type.
|
||||
# @see `type: field.storage_settings.*`
|
||||
field.field_settings.text_with_summary:
|
||||
type: mapping
|
||||
label: 'Text (formatted, long, with summary) settings'
|
||||
mapping:
|
||||
display_summary:
|
||||
type: boolean
|
||||
label: 'Summary input'
|
||||
required_summary:
|
||||
type: boolean
|
||||
label: 'Require summary'
|
||||
allowed_formats:
|
||||
type: sequence
|
||||
label: 'Allowed text formats'
|
||||
sequence:
|
||||
type: string
|
||||
|
||||
field.value.text_with_summary:
|
||||
type: mapping
|
||||
label: 'Default value'
|
||||
mapping:
|
||||
value:
|
||||
type: text
|
||||
label: 'Body'
|
||||
summary:
|
||||
type: string
|
||||
label: 'Summary'
|
||||
format:
|
||||
type: string
|
||||
label: 'Text format'
|
||||
|
||||
field.formatter.settings.text_default:
|
||||
type: mapping
|
||||
label: 'Formatted text default display format settings'
|
||||
|
||||
field.formatter.settings.text_summary_or_trimmed:
|
||||
type: mapping
|
||||
label: 'Summary or trimmed formatted text display format settings'
|
||||
mapping:
|
||||
trim_length:
|
||||
type: integer
|
||||
label: 'Trim length'
|
||||
|
||||
field.formatter.settings.text_trimmed:
|
||||
type: mapping
|
||||
label: 'Trimmed text display format settings'
|
||||
mapping:
|
||||
trim_length:
|
||||
type: integer
|
||||
label: 'Trim length'
|
||||
|
||||
field.widget.settings.text_textarea:
|
||||
type: mapping
|
||||
label: 'Text area (multiple rows) display format settings'
|
||||
mapping:
|
||||
rows:
|
||||
type: integer
|
||||
label: 'Rows'
|
||||
placeholder:
|
||||
type: label
|
||||
label: 'Placeholder'
|
||||
|
||||
field.widget.settings.text_textarea_with_summary:
|
||||
type: mapping
|
||||
label: 'Text area with a summary display format settings'
|
||||
mapping:
|
||||
rows:
|
||||
type: integer
|
||||
label: 'Rows'
|
||||
summary_rows:
|
||||
type: integer
|
||||
label: 'Number of summary rows'
|
||||
placeholder:
|
||||
type: text
|
||||
label: 'Placeholder'
|
||||
show_summary:
|
||||
type: boolean
|
||||
label: 'Always show summary'
|
||||
|
||||
field.widget.settings.text_textfield:
|
||||
type: mapping
|
||||
label: 'Text field display format settings'
|
||||
mapping:
|
||||
size:
|
||||
type: integer
|
||||
label: 'Size of textfield'
|
||||
placeholder:
|
||||
type: label
|
||||
label: 'Placeholder'
|
||||
9
web/core/modules/text/css/text.icon.theme.css
Normal file
9
web/core/modules/text/css/text.icon.theme.css
Normal 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");
|
||||
}
|
||||
3
web/core/modules/text/css/text.icon.theme.pcss.css
Normal file
3
web/core/modules/text/css/text.icon.theme.pcss.css
Normal file
@ -0,0 +1,3 @@
|
||||
.field-icon-formatted_text {
|
||||
background-image: url(../../../misc/icons/55565b/formatted_text.svg);
|
||||
}
|
||||
73
web/core/modules/text/js/text.js
Normal file
73
web/core/modules/text/js/text.js
Normal 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);
|
||||
@ -0,0 +1,5 @@
|
||||
finished:
|
||||
6:
|
||||
text: text
|
||||
7:
|
||||
text: text
|
||||
16
web/core/modules/text/migrations/text_settings.yml
Normal file
16
web/core/modules/text/migrations/text_settings.yml
Normal 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
|
||||
59
web/core/modules/text/src/Hook/TextHooks.php
Normal file
59
web/core/modules/text/src/Hook/TextHooks.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\text\Hook;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for text.
|
||||
*/
|
||||
class TextHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): ?string {
|
||||
switch ($route_name) {
|
||||
case 'help.page.text':
|
||||
$output = '';
|
||||
$output .= '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->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>' . $this->t('Uses') . '</h2>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . $this->t('Managing and displaying text fields') . '</dt>';
|
||||
$output .= '<dd>' . $this->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>' . $this->t('Creating short text fields') . '</dt>';
|
||||
$output .= '<dd>' . $this->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>' . $this->t('Creating long text fields') . '</dt>';
|
||||
$output .= '<dd>' . $this->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>' . $this->t('Trimming the text length') . '</dt>';
|
||||
$output .= '<dd>' . $this->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>' . $this->t('Displaying summaries instead of trimmed text') . '</dt>';
|
||||
$output .= '<dd>' . $this->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>' . $this->t('Using text formats and editors') . '</dt>';
|
||||
$output .= '<dd>' . $this->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;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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 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'];
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
100
web/core/modules/text/src/Plugin/Field/FieldType/TextItem.php
Normal file
100
web/core/modules/text/src/Plugin/Field/FieldType/TextItem.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 = (int) 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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]];
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
138
web/core/modules/text/src/Plugin/migrate/field/d6/TextField.php
Normal file
138
web/core/modules/text/src/Plugin/migrate/field/d6/TextField.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
112
web/core/modules/text/src/Plugin/migrate/field/d7/TextField.php
Normal file
112
web/core/modules/text/src/Plugin/migrate/field/d7/TextField.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Migrate field plugin for Drupal 7 text fields.
|
||||
*/
|
||||
#[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;
|
||||
}
|
||||
|
||||
}
|
||||
115
web/core/modules/text/src/TextProcessed.php
Normal file
115
web/core/modules/text/src/TextProcessed.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\text;
|
||||
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Serialization\Attribute\JsonSchema;
|
||||
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}
|
||||
*/
|
||||
#[JsonSchema(['type' => 'string', 'description' => 'May contain HTML markup.'])]
|
||||
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
|
||||
* The renderer service.
|
||||
*/
|
||||
protected function getRenderer() {
|
||||
return \Drupal::service('renderer');
|
||||
}
|
||||
|
||||
}
|
||||
14
web/core/modules/text/tests/src/Functional/GenericTest.php
Normal file
14
web/core/modules/text/tests/src/Functional/GenericTest.php
Normal 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 {}
|
||||
408
web/core/modules/text/tests/src/Functional/TextFieldTest.php
Normal file
408
web/core/modules/text/tests/src/Functional/TextFieldTest.php
Normal file
@ -0,0 +1,408 @@
|
||||
<?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
|
||||
*/
|
||||
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): void {
|
||||
// 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.
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
98
web/core/modules/text/tests/src/Kernel/TextFormatterTest.php
Normal file
98
web/core/modules/text/tests/src/Kernel/TextFormatterTest.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?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';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
107
web/core/modules/text/tests/src/Kernel/TextItemBaseTest.php
Normal file
107
web/core/modules/text/tests/src/Kernel/TextItemBaseTest.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?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', 'user'];
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('entity_test');
|
||||
$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());
|
||||
}
|
||||
|
||||
}
|
||||
365
web/core/modules/text/tests/src/Kernel/TextSummaryTest.php
Normal file
365
web/core/modules/text/tests/src/Kernel/TextSummaryTest.php
Normal file
@ -0,0 +1,365 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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('user');
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
<?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 {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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): void {
|
||||
// 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
<?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 {
|
||||
|
||||
/**
|
||||
* The migration field plugin to test.
|
||||
*
|
||||
* @var \Drupal\migrate_drupal\Plugin\MigrateFieldInterface
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* The migration object.
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?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 {
|
||||
|
||||
/**
|
||||
* The migration field plugin.
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
}
|
||||
6
web/core/modules/text/text.field_type_categories.yml
Normal file
6
web/core/modules/text/text.field_type_categories.yml
Normal 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
|
||||
8
web/core/modules/text/text.info.yml
Normal file
8
web/core/modules/text/text.info.yml
Normal file
@ -0,0 +1,8 @@
|
||||
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
|
||||
23
web/core/modules/text/text.libraries.yml
Normal file
23
web/core/modules/text/text.libraries.yml
Normal file
@ -0,0 +1,23 @@
|
||||
drupal.text:
|
||||
version: VERSION
|
||||
js:
|
||||
js/text.js: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/once
|
||||
- core/drupal
|
||||
moved_files:
|
||||
text/text.js:
|
||||
deprecation_version: drupal:11.1.0
|
||||
removed_version: drupal:12.0.0
|
||||
deprecation_link: https://www.drupal.org/node/3471539
|
||||
js:
|
||||
text.js: 'js/text.js'
|
||||
|
||||
drupal.text-icon:
|
||||
version: VERSION
|
||||
css:
|
||||
theme:
|
||||
css/text.icon.theme.css: {}
|
||||
dependencies:
|
||||
- field_ui/drupal.field_ui.manage_fields
|
||||
136
web/core/modules/text/text.module
Normal file
136
web/core/modules/text/text.module
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* 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 string $text
|
||||
* The content for which a summary will be generated.
|
||||
* @param string $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 string|null $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;
|
||||
}
|
||||
17
web/core/modules/text/text.post_update.php
Normal file
17
web/core/modules/text/text.post_update.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains post update hooks for the text module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_removed_post_updates().
|
||||
*/
|
||||
function text_removed_post_updates(): array {
|
||||
return [
|
||||
'text_post_update_add_required_summary_flag' => '9.0.0',
|
||||
'text_post_update_add_required_summary_flag_form_display' => '10.0.0',
|
||||
'text_post_update_allowed_formats' => '11.0.0',
|
||||
];
|
||||
}
|
||||
2
web/core/modules/text/text.services.yml
Normal file
2
web/core/modules/text/text.services.yml
Normal file
@ -0,0 +1,2 @@
|
||||
parameters:
|
||||
text.skip_procedural_hook_scan: true
|
||||
Reference in New Issue
Block a user