3 namespace Drupal\filter\Element;
5 use Drupal\Core\Form\FormStateInterface;
6 use Drupal\Core\Render\Element\RenderElement;
7 use Drupal\Core\Render\Element;
11 * Provides a text format render element.
14 * - #base_type: The form element #type to use for the 'value' element.
15 * 'textarea' by default.
16 * - #format: (optional) The text format ID to preselect. If omitted, the
17 * default format for the current user will be used.
18 * - #allowed_formats: (optional) An array of text format IDs that are available
19 * for this element. If omitted, all text formats that the current user has
20 * access to will be allowed.
24 * $form['body'] = array(
25 * '#type' => 'text_format',
27 * '#format' => 'full_html',
28 * '#default_value' => '<p>The quick brown fox jumped over the lazy dog.</p>',
32 * @see \Drupal\Core\Render\Element\Textarea
34 * @RenderElement("text_format")
36 class TextFormat extends RenderElement {
41 public function getInfo() {
42 $class = get_class($this);
45 [$class, 'processFormat'],
47 '#base_type' => 'textarea',
48 '#theme_wrappers' => ['text_format_wrapper'],
53 * Expands an element into a base element with text format selector attached.
55 * The form element will be expanded into two separate form elements, one
56 * holding the original element, and the other holding the text format
58 * - value: Holds the original element, having its #type changed to the value
59 * of #base_type or 'textarea' by default.
60 * - format: Holds the text format details and the text format selection,
61 * using the text format ID specified in #format or the user's default
62 * format by default, if NULL.
64 * The resulting value for the element will be an array holding the value and
65 * the format. For example, the value for the body element will be:
67 * $values = $form_state->getValue('body');
68 * $values['value'] = 'foo';
69 * $values['format'] = 'foo';
72 * @param array $element
73 * The form element to process. See main class documentation for properties.
74 * @param \Drupal\Core\Form\FormStateInterface $form_state
75 * The current state of the form.
76 * @param array $complete_form
77 * The complete form structure.
82 public static function processFormat(&$element, FormStateInterface $form_state, &$complete_form) {
83 $user = static::currentUser();
85 // Ensure that children appear as subkeys of this element.
86 $element['#tree'] = TRUE;
88 // Make \Drupal::formBuilder()->doBuildForm() regenerate child properties.
92 // Do not copy this #process function to prevent
93 // \Drupal::formBuilder()->doBuildForm() from recursing infinitely.
95 // Ensure #pre_render functions will be run.
97 // Description is handled by theme_text_format_wrapper().
99 // Ensure proper ordering of children.
101 // Properties already processed for the parent element.
108 // Move this element into sub-element 'value'.
109 unset($element['value']);
110 foreach (Element::properties($element) as $key) {
111 if (!in_array($key, $blacklist)) {
112 $element['value'][$key] = $element[$key];
116 $element['value']['#type'] = $element['#base_type'];
117 $element['value'] += static::elementInfo()->getInfo($element['#base_type']);
118 // Make sure the #default_value key is set, so we can use it below.
119 $element['value'] += ['#default_value' => ''];
121 // Turn original element into a text format wrapper.
122 $element['#attached']['library'][] = 'filter/drupal.filter';
124 // Setup child container for the text format widget.
125 $element['format'] = [
126 '#type' => 'container',
127 '#attributes' => ['class' => ['filter-wrapper']],
130 // Get a list of formats that the current user has access to.
131 $formats = filter_formats($user);
133 // Allow the list of formats to be restricted.
134 if (isset($element['#allowed_formats'])) {
135 // We do not add the fallback format here to allow the use-case of forcing
136 // certain text formats to be used for certain text areas. In case the
137 // fallback format is supposed to be allowed as well, it must be added to
138 // $element['#allowed_formats'] explicitly.
139 $formats = array_intersect_key($formats, array_flip($element['#allowed_formats']));
142 if (!isset($element['#format']) && !empty($formats)) {
143 // If no text format was selected, use the allowed format with the highest
144 // weight. This is equivalent to calling filter_default_format().
145 $element['#format'] = reset($formats)->id();
148 // If #allowed_formats is set, the list of formats must not be modified in
149 // any way. Otherwise, however, if all of the following conditions are true,
150 // remove the fallback format from the list of formats:
151 // 1. The 'always_show_fallback_choice' filter setting has not been activated.
152 // 2. Multiple text formats are available.
153 // 3. The fallback format is not the default format.
154 // The 'always_show_fallback_choice' filter setting is a hidden setting that
155 // has no UI. It defaults to FALSE.
156 $config = static::configFactory()->get('filter.settings');
157 if (!isset($element['#allowed_formats']) && !$config->get('always_show_fallback_choice')) {
158 $fallback_format = $config->get('fallback_format');
159 if ($element['#format'] !== $fallback_format && count($formats) > 1) {
160 unset($formats[$fallback_format]);
164 // Prepare text format guidelines.
165 $element['format']['guidelines'] = [
166 '#type' => 'container',
167 '#attributes' => ['class' => ['filter-guidelines']],
171 foreach ($formats as $format) {
172 $options[$format->id()] = $format->label();
173 $element['format']['guidelines'][$format->id()] = [
174 '#theme' => 'filter_guidelines',
175 '#format' => $format,
179 $element['format']['format'] = [
181 '#title' => t('Text format'),
182 '#options' => $options,
183 '#default_value' => $element['#format'],
184 '#access' => count($formats) > 1,
186 '#attributes' => ['class' => ['filter-list']],
187 '#parents' => array_merge($element['#parents'], ['format']),
190 $element['format']['help'] = [
191 '#type' => 'container',
194 '#title' => t('About text formats'),
195 '#url' => new Url('filter.tips_all'),
196 '#attributes' => ['target' => '_blank'],
198 '#attributes' => ['class' => ['filter-help']],
202 $all_formats = filter_formats();
203 $format_exists = isset($all_formats[$element['#format']]);
204 $format_allowed = !isset($element['#allowed_formats']) || in_array($element['#format'], $element['#allowed_formats']);
205 $user_has_access = isset($formats[$element['#format']]);
206 $user_is_admin = $user->hasPermission('administer filters');
208 // If the stored format does not exist or if it is not among the allowed
209 // formats for this textarea, administrators have to assign a new format.
210 if ((!$format_exists || !$format_allowed) && $user_is_admin) {
211 $element['format']['format']['#required'] = TRUE;
212 $element['format']['format']['#default_value'] = NULL;
213 // Force access to the format selector (it may have been denied above if
214 // the user only has access to a single format).
215 $element['format']['format']['#access'] = TRUE;
217 // Disable this widget, if the user is not allowed to use the stored format,
218 // or if the stored format does not exist. The 'administer filters'
219 // permission only grants access to the filter administration, not to all
221 elseif (!$user_has_access || !$format_exists) {
222 // Overload default values into #value to make them unalterable.
223 $element['value']['#value'] = $element['value']['#default_value'];
224 $element['format']['format']['#value'] = $element['format']['format']['#default_value'];
226 // Prepend #pre_render callback to replace field value with user notice
227 // prior to rendering.
228 $element['value'] += ['#pre_render' => []];
229 array_unshift($element['value']['#pre_render'], 'filter_form_access_denied');
231 // Cosmetic adjustments.
232 if (isset($element['value']['#rows'])) {
233 $element['value']['#rows'] = 3;
235 $element['value']['#disabled'] = TRUE;
236 $element['value']['#resizable'] = 'none';
238 // Hide the text format selector and any other child element (such as text
240 foreach (Element::children($element) as $key) {
241 if ($key != 'value') {
242 $element[$key]['#access'] = FALSE;
251 * Wraps the current user.
253 * \Drupal\Core\Session\AccountInterface
255 protected static function currentUser() {
256 return \Drupal::currentUser();
260 * Wraps the config factory.
262 * @return \Drupal\Core\Config\ConfigFactoryInterface
264 protected static function configFactory() {
265 return \Drupal::configFactory();
269 * Wraps the element info service.
271 * @return \Drupal\Core\Render\ElementInfoManagerInterface
273 protected static function elementInfo() {
274 return \Drupal::service('element_info');