Skip to content

Commit 4c09cdb

Browse files
author
Dominic Tubach
committed
Use a "text_format" element when a string of type "text/html" is requested
1 parent 27b175b commit 4c09cdb

File tree

6 files changed

+239
-4
lines changed

6 files changed

+239
-4
lines changed

src/Form/AbstractJsonFormsForm.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Drupal\Core\Form\FormStateInterface;
2626
use Drupal\json_forms\Form\Util\FieldNameUtil;
2727
use Drupal\json_forms\Form\Util\FormCallbackExecutor;
28+
use Drupal\json_forms\Form\Util\FormValidationUtil;
2829
use Drupal\json_forms\Form\Validation\FormValidationMapperInterface;
2930
use Drupal\json_forms\Form\Validation\FormValidatorInterface;
3031
use Drupal\json_forms\JsonForms\Definition\DefinitionFactory;
@@ -137,16 +138,28 @@ protected function buildJsonFormsForm(
137138
* @param \Drupal\Core\Form\FormStateInterface $formState
138139
*/
139140
public function validateForm(array &$form, FormStateInterface $formState): void {
141+
parent::validateForm($form, $formState);
142+
FormCallbackExecutor::executePreSchemaValidationCallbacks($formState);
143+
140144
if ($formState->isSubmitted() || $formState->isValidationEnforced()) {
141145
if (TRUE === $formState->get('$limitValidationUsed')) {
142146
// We cannot use Drupal validation errors if the form uses limited
143147
// validation. They might contain errors that with the submitted data
144148
// would be ignored. (Avoiding Drupal validation is not possible on form
145149
// submit.)
150+
$keepFormErrorElementKeys = FormValidationUtil::getKeepFormErrorElementKeys($formState);
151+
$keepFormErrors = array_map(
152+
fn (array $elementKey) => $formState->getError(['#parents' => $elementKey]),
153+
$keepFormErrorElementKeys
154+
);
146155
$formState->clearErrors();
156+
foreach ($keepFormErrorElementKeys as $index => $elementKey) {
157+
if (NULL !== $keepFormErrors[$index]) {
158+
$element = ['#parents' => $elementKey];
159+
$formState->setError($element, $keepFormErrors[$index]);
160+
}
161+
}
147162
}
148-
parent::validateForm($form, $formState);
149-
FormCallbackExecutor::executePreSchemaValidationCallbacks($formState);
150163
$validationResult = $this->formValidator->validate(
151164
// @phpstan-ignore-next-line
152165
$formState->get('jsonSchema'),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* Copyright (C) 2022 SYSTOPIA GmbH
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 2 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
declare(strict_types=1);
21+
22+
namespace Drupal\json_forms\Form\Control\Callbacks;
23+
24+
use Drupal\Core\Form\FormStateInterface;
25+
26+
final class HtmlCallbacks {
27+
28+
/**
29+
* Converts the value set by the "text_format" form element at the given key.
30+
* This method is called after Drupal validation.
31+
*
32+
* @phpstan-param array<int|string> $elementKey
33+
*/
34+
public static function convertValue(FormStateInterface $formState, string $callbackKey, array $elementKey): void {
35+
/** @var array{value: string, format: string} $value */
36+
$value = $formState->getValue($elementKey);
37+
$formState->setValue($elementKey, $value['value'] ?? NULL);
38+
}
39+
40+
}

src/Form/Control/HtmlArrayFactory.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/*
4+
* Copyright (C) 2025 SYSTOPIA GmbH
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 2 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
declare(strict_types=1);
21+
22+
namespace Drupal\json_forms\Form\Control;
23+
24+
use Assert\Assertion;
25+
use Drupal\Core\Form\FormStateInterface;
26+
use Drupal\json_forms\Form\AbstractConcreteFormArrayFactory;
27+
use Drupal\json_forms\Form\Control\Callbacks\HtmlCallbacks;
28+
use Drupal\json_forms\Form\Control\Callbacks\StringValueCallback;
29+
use Drupal\json_forms\Form\Control\Util\BasicFormPropertiesFactory;
30+
use Drupal\json_forms\Form\FormArrayFactoryInterface;
31+
use Drupal\json_forms\Form\Util\FormCallbackRegistrator;
32+
use Drupal\json_forms\Form\Util\FormValidationUtil;
33+
use Drupal\json_forms\JsonForms\Definition\Control\ControlDefinition;
34+
use Drupal\json_forms\JsonForms\Definition\Control\StringControlDefinition;
35+
use Drupal\json_forms\JsonForms\Definition\DefinitionInterface;
36+
37+
class HtmlArrayFactory extends AbstractConcreteFormArrayFactory {
38+
39+
public static function getPriority(): int {
40+
return StringArrayFactory::getPriority() + 1;
41+
}
42+
43+
/**
44+
* {@inheritDoc}
45+
*/
46+
public function createFormArray(
47+
DefinitionInterface $definition,
48+
FormStateInterface $formState,
49+
FormArrayFactoryInterface $formArrayFactory
50+
): array {
51+
Assertion::isInstanceOf($definition, ControlDefinition::class);
52+
/** @var \Drupal\json_forms\JsonForms\Definition\Control\ControlDefinition $definition */
53+
$definition = StringControlDefinition::fromDefinition($definition);
54+
/** @var \Drupal\json_forms\JsonForms\Definition\Control\StringControlDefinition $definition */
55+
56+
$form = [
57+
'#type' => 'text_format',
58+
'#value_callback' => StringValueCallback::class . '::convert',
59+
] + BasicFormPropertiesFactory::createFieldProperties($definition, $formState);
60+
61+
if (NULL !== $definition->getMaxLength()) {
62+
$form['#maxlength'] = $definition->getMaxLength();
63+
}
64+
65+
if (NULL !== $definition->getPattern()) {
66+
$form['#pattern'] = $definition->getPattern();
67+
}
68+
69+
/** @var list<int|string> $elementKey */
70+
$elementKey = $form['#parents'];
71+
FormValidationUtil::addFormErrorMapping($formState, $elementKey, array_merge($elementKey, ['value']));
72+
FormValidationUtil::addKeepFormErrorElementKey($formState, array_merge($elementKey, ['format']));
73+
74+
FormCallbackRegistrator::registerPreSchemaValidationCallback(
75+
$formState,
76+
$definition->getFullScope(),
77+
[HtmlCallbacks::class, 'convertValue'],
78+
$form['#parents'],
79+
);
80+
81+
return $form;
82+
}
83+
84+
public function supportsDefinition(DefinitionInterface $definition): bool {
85+
return $definition instanceof ControlDefinition && 'string' === $definition->getType()
86+
&& 'text/html' === $definition->getContentMediaType();
87+
}
88+
89+
}

src/Form/Util/FormValidationUtil.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/*
4+
* Copyright (C) 2025 SYSTOPIA GmbH
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 2 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
declare(strict_types=1);
21+
22+
namespace Drupal\json_forms\Form\Util;
23+
24+
use Drupal\Core\Form\FormStateInterface;
25+
26+
final class FormValidationUtil {
27+
28+
/**
29+
* Allows to use a different form element to add a validation error than the
30+
* JSON schema validation would use. Useful when a Drupal form element makes
31+
* use of sub-elements, e.g. "text_format" uses the sub-elements "value" and
32+
* "format" where "value" contains the value validated by the JSON schema. So
33+
* validation errors detected by the JSON schema validator should be added to
34+
* the "value" element.
35+
*
36+
* @param list<int|string> $key
37+
* Value of the "#parents" attribute.
38+
* @param list<int|string> $targetKey
39+
* Value of the "#parents" attribute an error should be added to.
40+
*/
41+
public static function addFormErrorMapping(FormStateInterface $formState, array $key, array $targetKey): void {
42+
$formState->set(['formErrorMapping', implode('/', $key)], $targetKey);
43+
}
44+
45+
/**
46+
* @param list<int|string> $key
47+
*
48+
* @return list<int|string>
49+
* The element key ("#parents" attribute) an error for the element with the
50+
* given key should be added to. If no mapping is defined, the given key is
51+
* returned.
52+
*/
53+
public static function getFormErrorMapping(FormStateInterface $formState, array $key): array {
54+
// @phpstan-ignore return.type
55+
return $formState->get(['formErrorMapping', implode('/', $key)]) ?? $key;
56+
}
57+
58+
/**
59+
* Allows to keep Drupal validation errors when the "$limitValidation" keyword
60+
* is used. Normally, all Drupal validation errors are cleared when
61+
* "$limitValidation" is used. This can be used to keep errors for fields that
62+
* cannot be validated by the JSON schema, e.g. the "format" sub-element of a
63+
* "text_format" element.
64+
*
65+
* @param list<int|string> $key
66+
* The form element key ("#parents" attribute) for which to keep Drupal
67+
* validation errors.
68+
*/
69+
public static function addKeepFormErrorElementKey(FormStateInterface $formState, array $key): void {
70+
$formState->set(['keepFormErrorElements', implode('/', $key)], $key);
71+
}
72+
73+
/**
74+
* @return list<list<int|string>>
75+
* List of element keys ("#parents" attribute) for which to keep Drupal
76+
* validation errors.
77+
*/
78+
public static function getKeepFormErrorElementKeys(FormStateInterface $formState): array {
79+
// @phpstan-ignore argument.type, return.type
80+
return array_values($formState->get('keepFormErrorElements') ?? []);
81+
}
82+
83+
}

src/Form/Validation/FormValidationMapper.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Assert\Assertion;
2525
use Drupal\Core\Form\FormStateInterface;
2626
use Drupal\json_forms\Form\Util\FieldNameUtil;
27+
use Drupal\json_forms\Form\Util\FormValidationUtil;
2728
use Drupal\json_forms\Form\Util\FormValueAccessor;
2829
use Opis\JsonSchema\JsonPointer;
2930

@@ -40,8 +41,13 @@ public function mapErrors(ValidationResult $validationResult, FormStateInterface
4041
foreach ($validationResult->getLeafErrorMessages() as $pointer => $messages) {
4142
$pointer = JsonPointer::parse($pointer);
4243
Assertion::notNull($pointer);
43-
// @phpstan-ignore-next-line
44-
$element = ['#parents' => FieldNameUtil::toFormParents($pointer->absolutePath())];
44+
$element = [
45+
'#parents' => FormValidationUtil::getFormErrorMapping(
46+
$formState,
47+
// @phpstan-ignore argument.type
48+
FieldNameUtil::toFormParents($pointer->absolutePath())
49+
),
50+
];
4551
$formState->setError($element, implode("\n", $messages));
4652
}
4753
}

src/JsonForms/Definition/Control/ControlDefinition.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ public function getConst() {
123123
return NULL;
124124
}
125125

126+
public function getContentMediaType(): ?string {
127+
return $this->propertySchema->contentMediaType ?? NULL;
128+
}
129+
126130
public function getControlFormat(): ?string {
127131
return $this->controlSchema->options->format ?? NULL;
128132
}

0 commit comments

Comments
 (0)