[TASK] Render wizards of FormEngine elements only if needed
[Packages/TYPO3.CMS.git] / typo3 / sysext / rte_ckeditor / Classes / Form / Element / RichTextElement.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\RteCKEditor\Form\Element;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
19 use TYPO3\CMS\Backend\Routing\UriBuilder;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Localization\Locales;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\PathUtility;
24
25 /**
26 * Render rich text editor in FormEngine
27 */
28 class RichTextElement extends AbstractFormElement
29 {
30 /**
31 * Default field information enabled for this element.
32 *
33 * @var array
34 */
35 protected $defaultFieldInformation = [
36 'tcaDescription' => [
37 'renderType' => 'tcaDescription',
38 ],
39 ];
40
41 /**
42 * Default field wizards enabled for this element.
43 *
44 * @var array
45 */
46 protected $defaultFieldWizard = [
47 'localizationStateSelector' => [
48 'renderType' => 'localizationStateSelector',
49 ],
50 'otherLanguageContent' => [
51 'renderType' => 'otherLanguageContent',
52 'after' => [
53 'localizationStateSelector'
54 ],
55 ],
56 'defaultLanguageDifferences' => [
57 'renderType' => 'defaultLanguageDifferences',
58 'after' => [
59 'otherLanguageContent',
60 ],
61 ],
62 ];
63
64 /**
65 * This property contains configuration related to the RTE
66 * But only the .editor configuration part
67 *
68 * @var array
69 */
70 protected $rteConfiguration = [];
71
72 /**
73 * Renders the ckeditor element
74 *
75 * @return array
76 * @throws \InvalidArgumentException
77 */
78 public function render(): array
79 {
80 $resultArray = $this->initializeResultArray();
81 $parameterArray = $this->data['parameterArray'];
82 $config = $parameterArray['fieldConf']['config'];
83
84 $fieldId = $this->sanitizeFieldId($parameterArray['itemFormElName']);
85 $itemFormElementName = $this->data['parameterArray']['itemFormElName'];
86
87 $value = $this->data['parameterArray']['itemFormElValue'] ?? '';
88
89 $fieldInformationResult = $this->renderFieldInformation();
90 $fieldInformationHtml = $fieldInformationResult['html'];
91 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
92
93 $fieldControlResult = $this->renderFieldControl();
94 $fieldControlHtml = $fieldControlResult['html'];
95 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
96
97 $fieldWizardResult = $this->renderFieldWizard();
98 $fieldWizardHtml = $fieldWizardResult['html'];
99 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
100
101 $attributes = [
102 'style' => 'display:none',
103 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
104 'id' => $fieldId,
105 'name' => htmlspecialchars($itemFormElementName),
106 ];
107
108 $html = [];
109 $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
110 $html[] = $fieldInformationHtml;
111 $html[] = '<div class="form-control-wrap">';
112 $html[] = '<div class="form-wizards-wrap">';
113 $html[] = '<div class="form-wizards-element">';
114 $html[] = '<textarea ' . GeneralUtility::implodeAttributes($attributes, true) . '>';
115 $html[] = htmlspecialchars($value);
116 $html[] = '</textarea>';
117 $html[] = '</div>';
118 if (!empty($fieldControlHtml)) {
119 $html[] = '<div class="form-wizards-items-aside">';
120 $html[] = '<div class="btn-group">';
121 $html[] = $fieldControlHtml;
122 $html[] = '</div>';
123 $html[] = '</div>';
124 }
125 if (!empty($fieldWizardHtml)) {
126 $html[] = '<div class="form-wizards-items-bottom">';
127 $html[] = $fieldWizardHtml;
128 $html[] = '</div>';
129 }
130 $html[] = '</div>';
131 $html[] = '</div>';
132 $html[] = '</div>';
133
134 $resultArray['html'] = implode(LF, $html);
135
136 $this->rteConfiguration = $config['richtextConfiguration']['editor'];
137 $resultArray['requireJsModules'][] = [
138 'ckeditor' => $this->getCkEditorRequireJsModuleCode($fieldId)
139 ];
140
141 return $resultArray;
142 }
143
144 /**
145 * Determine the contents language iso code
146 *
147 * @return string
148 */
149 protected function getLanguageIsoCodeOfContent(): string
150 {
151 $currentLanguageUid = $this->data['databaseRow']['sys_language_uid'];
152 if (is_array($currentLanguageUid)) {
153 $currentLanguageUid = $currentLanguageUid[0];
154 }
155 $contentLanguageUid = (int)max($currentLanguageUid, 0);
156 if ($contentLanguageUid) {
157 $contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'];
158 } else {
159 $contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en_US';
160 $languageCodeParts = explode('_', $contentLanguage);
161 $contentLanguage = strtolower($languageCodeParts[0]) . ($languageCodeParts[1] ? '_' . strtoupper($languageCodeParts[1]) : '');
162 // Find the configured language in the list of localization locales
163 $locales = GeneralUtility::makeInstance(Locales::class);
164 // If not found, default to 'en'
165 if (!in_array($contentLanguage, $locales->getLocales(), true)) {
166 $contentLanguage = 'en';
167 }
168 }
169 return $contentLanguage;
170 }
171
172 /**
173 * Gets the JavaScript code for CKEditor module
174 * Compiles the configuration, and then adds plugins
175 *
176 * @param string $fieldId
177 * @return string
178 */
179 protected function getCkEditorRequireJsModuleCode(string $fieldId): string
180 {
181 $configuration = $this->prepareConfigurationForEditor();
182
183 $externalPlugins = '';
184 foreach ($this->getExtraPlugins() as $pluginName => $config) {
185 if (!empty($config['config']) && !empty($configuration[$pluginName])) {
186 $config['config'] = array_replace_recursive($config['config'], $configuration[$pluginName]);
187 }
188 $configuration[$pluginName] = $config['config'];
189 $configuration['extraPlugins'] .= ',' . $pluginName;
190
191 $externalPlugins .= 'CKEDITOR.plugins.addExternal(';
192 $externalPlugins .= GeneralUtility::quoteJSvalue($pluginName) . ',';
193 $externalPlugins .= GeneralUtility::quoteJSvalue($config['resource']) . ',';
194 $externalPlugins .= '\'\');';
195 }
196
197 return 'function(CKEDITOR) {
198 ' . $externalPlugins . '
199 $(function(){
200 CKEDITOR.replace("' . $fieldId . '", ' . json_encode($configuration) . ');
201 require([\'jquery\', \'TYPO3/CMS/Backend/FormEngine\'], function($, FormEngine) {
202 CKEDITOR.instances["' . $fieldId . '"].on(\'change\', function() {
203 CKEDITOR.instances["' . $fieldId . '"].updateElement();
204 FormEngine.Validation.validate();
205 FormEngine.Validation.markFieldAsChanged($(\'#' . $fieldId . '\'));
206 });
207 $(document).on(\'inline:sorting-changed\', function() {
208 CKEDITOR.instances["' . $fieldId . '"].destroy();
209 CKEDITOR.replace("' . $fieldId . '", ' . json_encode($configuration) . ');
210 });
211 $(document).on(\'flexform:sorting-changed\', function() {
212 CKEDITOR.instances["' . $fieldId . '"].destroy();
213 CKEDITOR.replace("' . $fieldId . '", ' . json_encode($configuration) . ');
214 });
215 });
216 });
217 }';
218 }
219
220 /**
221 * Get configuration of external/additional plugins
222 *
223 * @return array
224 */
225 protected function getExtraPlugins(): array
226 {
227 $urlParameters = [
228 'P' => [
229 'table' => $this->data['tableName'],
230 'uid' => $this->data['databaseRow']['uid'],
231 'fieldName' => $this->data['fieldName'],
232 'recordType' => $this->data['recordTypeValue'],
233 'pid' => $this->data['effectivePid'],
234 'richtextConfigurationName' => $this->data['parameterArray']['fieldConf']['config']['richtextConfigurationName']
235 ]
236 ];
237
238 $pluginConfiguration = [];
239 if (isset($this->rteConfiguration['externalPlugins']) && is_array($this->rteConfiguration['externalPlugins'])) {
240 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
241 foreach ($this->rteConfiguration['externalPlugins'] as $pluginName => $configuration) {
242 $pluginConfiguration[$pluginName] = [
243 'resource' => $this->resolveUrlPath($configuration['resource'])
244 ];
245 unset($configuration['resource']);
246
247 if ($configuration['route']) {
248 $configuration['routeUrl'] = (string)$uriBuilder->buildUriFromRoute($configuration['route'], $urlParameters);
249 }
250
251 $pluginConfiguration[$pluginName]['config'] = $configuration;
252 }
253 }
254 return $pluginConfiguration;
255 }
256
257 /**
258 * Add configuration to replace LLL: references with the translated value
259 * @param array $configuration
260 *
261 * @return array
262 */
263 protected function replaceLanguageFileReferences(array $configuration): array
264 {
265 foreach ($configuration as $key => $value) {
266 if (is_array($value)) {
267 $configuration[$key] = $this->replaceLanguageFileReferences($value);
268 } elseif (is_string($value) && stripos($value, 'LLL:') === 0) {
269 $configuration[$key] = $this->getLanguageService()->sL($value);
270 }
271 }
272 return $configuration;
273 }
274
275 /**
276 * Add configuration to replace absolute EXT: paths with relative ones
277 * @param array $configuration
278 *
279 * @return array
280 */
281 protected function replaceAbsolutePathsToRelativeResourcesPath(array $configuration): array
282 {
283 foreach ($configuration as $key => $value) {
284 if (is_array($value)) {
285 $configuration[$key] = $this->replaceAbsolutePathsToRelativeResourcesPath($value);
286 } elseif (is_string($value) && stripos($value, 'EXT:') === 0) {
287 $configuration[$key] = $this->resolveUrlPath($value);
288 }
289 }
290 return $configuration;
291 }
292
293 /**
294 * Resolves an EXT: syntax file to an absolute web URL
295 *
296 * @param string $value
297 * @return string
298 */
299 protected function resolveUrlPath(string $value): string
300 {
301 $value = GeneralUtility::getFileAbsFileName($value);
302 return PathUtility::getAbsoluteWebPath($value);
303 }
304
305 /**
306 * Compiles the configuration set from the outside
307 * to have it easily injected into the CKEditor.
308 *
309 * @return array the configuration
310 */
311 protected function prepareConfigurationForEditor(): array
312 {
313 // Ensure custom config is empty so nothing additional is loaded
314 // Of course this can be overridden by the editor configuration below
315 $configuration = [
316 'customConfig' => '',
317 ];
318
319 if (is_array($this->rteConfiguration['config'])) {
320 $configuration = array_replace_recursive($configuration, $this->rteConfiguration['config']);
321 }
322 // Set the UI language of the editor if not hard-coded by the existing configuration
323 if (empty($configuration['language'])) {
324 $configuration['language'] = $this->getBackendUser()->uc['lang'] ?: ($this->getBackendUser()->user['lang'] ?: 'en');
325 }
326 $configuration['contentsLanguage'] = $this->getLanguageIsoCodeOfContent();
327
328 // Replace all label references
329 $configuration = $this->replaceLanguageFileReferences($configuration);
330 // Replace all paths
331 $configuration = $this->replaceAbsolutePathsToRelativeResourcesPath($configuration);
332
333 // there are some places where we define an array, but it needs to be a list in order to work
334 if (is_array($configuration['extraPlugins'])) {
335 $configuration['extraPlugins'] = implode(',', $configuration['extraPlugins']);
336 }
337 if (is_array($configuration['removePlugins'])) {
338 $configuration['removePlugins'] = implode(',', $configuration['removePlugins']);
339 }
340 if (is_array($configuration['removeButtons'])) {
341 $configuration['removeButtons'] = implode(',', $configuration['removeButtons']);
342 }
343
344 return $configuration;
345 }
346
347 /**
348 * @param string $itemFormElementName
349 * @return string
350 */
351 protected function sanitizeFieldId(string $itemFormElementName): string
352 {
353 $fieldId = preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $itemFormElementName);
354 return htmlspecialchars(preg_replace('/^[^a-zA-Z]/', 'x', $fieldId));
355 }
356
357 /**
358 * @return BackendUserAuthentication
359 */
360 protected function getBackendUser()
361 {
362 return $GLOBALS['BE_USER'];
363 }
364 }