From f835b728943fa5788739f1c67f8df7e690502a20 Mon Sep 17 00:00:00 2001
From: Frank Naegler <frank.naegler@typo3.org>
Date: Thu, 9 Feb 2023 12:38:46 +0100
Subject: [PATCH] [FEATURE] Introduce TCA type "json"

A new TCA type "json" is introduced, which
allows to simplify the configuration when
working with fields, containing JSON data.

It replaces the previously introduced dbtype=json
of TCA type "user" (see: #99226).

FormEngine will display the JSON data using
codemirror, in case EXT:t3editor is installed
and enabled for the field. Otherwise, a standard
textarea HTML element is used.

Using the new type, corresponding database
columns are added automatically.

Resolves: #100088
Related: #99226
Releases: main
Change-Id: I49ab049966a86696910a7c18a7c45be00c162ca7
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77798
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Nikita Hovratov <nikita.h@live.de>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Nikita Hovratov <nikita.h@live.de>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../form-engine/element/json-element.ts       |  45 ++++
 .../t3editor/element/code-mirror-element.ts   |  16 +-
 .../Classes/Form/Element/JsonElement.php      | 247 ++++++++++++++++++
 .../TcaColumnsProcessPlaceholders.php         |   1 +
 .../FormDataProvider/TcaInputPlaceholders.php |   1 +
 .../Classes/Form/FormDataProvider/TcaJson.php |  51 ++++
 .../Form/FormDataProvider/TcaRecordTitle.php  |   3 +
 .../backend/Classes/Form/NodeFactory.php      |   1 +
 .../Form/Utility/FormEngineUtility.php        |   1 +
 .../Classes/RecordList/DatabaseRecordList.php |   2 +
 .../LiveSearch/DatabaseRecordProvider.php     |   1 +
 .../Classes/Utility/BackendUtility.php        |  38 +--
 .../form-engine/element/json-element.js       |  13 +
 .../Unit/Form/Element/JsonElementTest.php     | 118 +++++++++
 .../Form/FormDataProvider/TcaJsonTest.php     | 241 +++++++++++++++++
 .../core/Classes/DataHandling/DataHandler.php |  42 ++-
 .../Classes/DataHandling/TableColumnType.php  |   1 +
 .../Database/Schema/DefaultTcaSchema.php      | 148 ++++++-----
 .../SearchTermRestriction.php                 |   1 +
 .../Configuration/DefaultConfiguration.php    |  19 +-
 ...9226-IntroduceDbTypeJsonForTCATypeUser.rst |   8 +
 .../12.3/Feature-100088-NewTCATypeJson.rst    |  58 ++++
 ...-100088-RemoveDbTypeJsonForTCATypeUser.rst |  48 ++++
 .../Unit/DataHandling/DataHandlerTest.php     |  36 +++
 .../Generic/Mapper/DataMapFactoryTest.php     |   1 +
 .../DatabaseIntegrityController.php           |   2 +
 .../Classes/Form/Element/FieldMapElement.php  |   2 +-
 .../Overrides/sys_reaction_create_record.php  |   4 +-
 typo3/sysext/reactions/ext_tables.sql         |   1 -
 .../Configuration/Backend/T3editor/Modes.php  |   4 +
 .../JavaScript/element/code-mirror-element.js |   6 +-
 31 files changed, 1062 insertions(+), 98 deletions(-)
 create mode 100644 Build/Sources/TypeScript/backend/form-engine/element/json-element.ts
 create mode 100644 typo3/sysext/backend/Classes/Form/Element/JsonElement.php
 create mode 100644 typo3/sysext/backend/Classes/Form/FormDataProvider/TcaJson.php
 create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/form-engine/element/json-element.js
 create mode 100644 typo3/sysext/backend/Tests/Unit/Form/Element/JsonElementTest.php
 create mode 100644 typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaJsonTest.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Feature-100088-NewTCATypeJson.rst
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.3/Important-100088-RemoveDbTypeJsonForTCATypeUser.rst

diff --git a/Build/Sources/TypeScript/backend/form-engine/element/json-element.ts b/Build/Sources/TypeScript/backend/form-engine/element/json-element.ts
new file mode 100644
index 000000000000..0a3b678e48e1
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/form-engine/element/json-element.ts
@@ -0,0 +1,45 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import {Resizable} from './modifier/resizable';
+import {Tabbable} from './modifier/tabbable';
+
+/**
+ * Module: @typo3/backend/form-engine/element/json-element
+ *
+ * Functionality for the json element
+ *
+ * @example
+ * <typo3-formengine-element-json recordFieldId="some-id">
+ *   ...
+ * </typo3-formengine-element-json>
+ *
+ * This is based on W3C custom elements ("web components") specification, see
+ * https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
+ */
+class JsonElement extends HTMLElement {
+  private element: HTMLTextAreaElement = null;
+
+  public connectedCallback(): void {
+    this.element = document.getElementById((this.getAttribute('recordFieldId') || '' as string)) as HTMLTextAreaElement;
+
+    if (!this.element) {
+      return;
+    }
+
+    Resizable.enable(this.element);
+    Tabbable.enable(this.element);
+  }
+}
+
+window.customElements.define('typo3-formengine-element-json', JsonElement);
diff --git a/Build/Sources/TypeScript/t3editor/element/code-mirror-element.ts b/Build/Sources/TypeScript/t3editor/element/code-mirror-element.ts
index b3aa02fd3326..f5a64d6d0eb2 100644
--- a/Build/Sources/TypeScript/t3editor/element/code-mirror-element.ts
+++ b/Build/Sources/TypeScript/t3editor/element/code-mirror-element.ts
@@ -13,7 +13,16 @@
 
 import {LitElement, html, css} from 'lit';
 import {customElement, property, state} from 'lit/decorators';
-import {EditorView, ViewUpdate, lineNumbers, highlightSpecialChars, drawSelection, keymap, KeyBinding} from '@codemirror/view';
+import {
+  EditorView,
+  ViewUpdate,
+  lineNumbers,
+  highlightSpecialChars,
+  drawSelection,
+  keymap,
+  KeyBinding,
+  placeholder
+} from '@codemirror/view';
 import {Extension, EditorState} from '@codemirror/state';
 import {syntaxHighlighting, defaultHighlightStyle} from '@codemirror/language';
 import {defaultKeymap,  indentWithTab} from '@codemirror/commands';
@@ -47,6 +56,7 @@ export class CodeMirrorElement extends LitElement {
   @property({type: Boolean, reflect: true}) fullscreen: boolean = false;
 
   @property({type: String}) label: string;
+  @property({type: String}) placeholder: string;
   @property({type: String}) panel: string = 'bottom';
 
   @state() editorView: EditorView = null;
@@ -185,6 +195,10 @@ export class CodeMirrorElement extends LitElement {
       extensions.push(EditorState.readOnly.of(true));
     }
 
+    if (this.placeholder) {
+      extensions.push(placeholder(this.placeholder));
+    }
+
     if (this.mode) {
       const modeImplementation = <Extension[]>await executeJavaScriptModuleInstruction(this.mode);
       extensions.push(...modeImplementation);
diff --git a/typo3/sysext/backend/Classes/Form/Element/JsonElement.php b/typo3/sysext/backend/Classes/Form/Element/JsonElement.php
new file mode 100644
index 000000000000..5551ad417023
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Form/Element/JsonElement.php
@@ -0,0 +1,247 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Form\Element;
+
+use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\T3editor\Registry\AddonRegistry;
+use TYPO3\CMS\T3editor\Registry\ModeRegistry;
+use TYPO3\CMS\T3editor\T3editor;
+
+/**
+ * Handles type=json elements.
+ *
+ * Renders either a code editor or a standard textarea.
+ */
+class JsonElement extends AbstractFormElement
+{
+    /**
+     * Default field information enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldInformation = [
+        'tcaDescription' => [
+            'renderType' => 'tcaDescription',
+        ],
+    ];
+
+    /**
+     * Default field wizards enabled for this element.
+     *
+     * @var array
+     */
+    protected $defaultFieldWizard = [
+        'localizationStateSelector' => [
+            'renderType' => 'localizationStateSelector',
+        ],
+        'otherLanguageContent' => [
+            'renderType' => 'otherLanguageContent',
+            'after' => [
+                'localizationStateSelector',
+            ],
+        ],
+        'defaultLanguageDifferences' => [
+            'renderType' => 'defaultLanguageDifferences',
+            'after' => [
+                'otherLanguageContent',
+            ],
+        ],
+    ];
+
+    public function render(): array
+    {
+        $resultArray = $this->initializeResultArray();
+
+        $parameterArray = $this->data['parameterArray'];
+        $config = $parameterArray['fieldConf']['config'];
+        $readOnly = (bool)($config['readOnly'] ?? false);
+        $placeholder = trim((string)($config['placeholder'] ?? ''));
+        $enableCodeEditor = ($config['enableCodeEditor'] ?? true) && ExtensionManagementUtility::isLoaded('t3editor');
+
+        $itemValue = '';
+        if (!empty($parameterArray['itemFormElValue'])) {
+            try {
+                $itemValue = (string)json_encode($parameterArray['itemFormElValue'], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
+            } catch (\JsonException) {
+            }
+        }
+
+        $width = null;
+        if ($config['cols'] ?? false) {
+            $width = $this->formMaxWidth(MathUtility::forceIntegerInRange($config['cols'], $this->minimumInputWidth, $this->maxInputWidth));
+        }
+
+        $rows = MathUtility::forceIntegerInRange(($config['rows'] ?? 5) ?: 5, 1, 20);
+        $originalRows = $rows;
+        if (($itemFormElementValueLength = strlen($itemValue)) > 80) {
+            $calculatedRows = MathUtility::forceIntegerInRange(
+                (int)round($itemFormElementValueLength / 40),
+                count(explode(LF, $itemValue)),
+                20
+            );
+            if ($originalRows < $calculatedRows) {
+                $rows = $calculatedRows;
+            }
+        }
+
+        $fieldInformationResult = $this->renderFieldInformation();
+        $fieldInformationHtml = $fieldInformationResult['html'];
+        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
+
+        // Early return readonly display in case t3editor is not available
+        if ($readOnly && !$enableCodeEditor) {
+            $html = [];
+            $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
+            $html[] =   $fieldInformationHtml;
+            $html[] =   '<div class="form-wizards-wrap">';
+            $html[] =       '<div class="form-wizards-element">';
+            $html[] =           '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px">' : '>');
+            $html[] =               '<textarea class="form-control text-monospace" rows="' . $rows . '" disabled>';
+            $html[] =                   htmlspecialchars($itemValue);
+            $html[] =               '</textarea>';
+            $html[] =           '</div>';
+            $html[] =       '</div>';
+            $html[] =   '</div>';
+            $html[] = '</div>';
+            $resultArray['html'] = implode(LF, $html);
+            return $resultArray;
+        }
+
+        $fieldId = StringUtility::getUniqueId('formengine-json-');
+        $itemName = (string)$parameterArray['itemFormElName'];
+        $attributes = [
+            'id' => $fieldId,
+            'name' => $itemName,
+            'wrap' => 'off',
+            'rows' => (string)$rows,
+            'class' => 'form-control text-monospace',
+            'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
+        ];
+
+        if ($readOnly) {
+            $attributes['disabled'] = '';
+        }
+
+        if ($placeholder !== '') {
+            $attributes['placeholder'] = $placeholder;
+        }
+
+        // Use CodeMirror if available
+        if ($enableCodeEditor) {
+            // Compile and register t3editor configuration
+            GeneralUtility::makeInstance(T3editor::class)->registerConfiguration();
+
+            $modeRegistry = GeneralUtility::makeInstance(ModeRegistry::class);
+            $mode = $modeRegistry->isRegistered('json')
+                ? $modeRegistry->getByFormatCode('json')
+                : $modeRegistry->getDefaultMode();
+
+            $addons = $keymaps = [];
+            foreach (GeneralUtility::makeInstance(AddonRegistry::class)->getAddons() as $addon) {
+                foreach ($addon->getCssFiles() as $cssFile) {
+                    $resultArray['stylesheetFiles'][] = $cssFile;
+                }
+                if (($module = $addon->getModule())) {
+                    $addons[] = $module;
+                }
+                if (($keymap = $addon->getKeymap())) {
+                    $keymaps[] = $keymap;
+                }
+            }
+
+            $codeMirrorConfig = [
+                'label' => $this->data['tableName'] . ' > ' . $this->data['fieldName'],
+                'mode' => GeneralUtility::jsonEncodeForHtmlAttribute($mode->getModule(), false),
+            ];
+
+            if ($readOnly) {
+                $codeMirrorConfig['readonly'] = '';
+            }
+            if ($placeholder !== '') {
+                $codeMirrorConfig['placeholder'] = $placeholder;
+            }
+            if ($addons !== []) {
+                $codeMirrorConfig['addons'] = GeneralUtility::jsonEncodeForHtmlAttribute($addons, false);
+            }
+            if ($keymaps !== []) {
+                $codeMirrorConfig['keymaps'] = GeneralUtility::jsonEncodeForHtmlAttribute($keymaps, false);
+            }
+
+            $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/t3editor/element/code-mirror-element.js');
+            $editorHtml = '
+                <typo3-t3editor-codemirror ' . GeneralUtility::implodeAttributes($codeMirrorConfig, true, true) . '>
+                    <textarea ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . htmlspecialchars($itemValue) . '</textarea>
+                    <input type="hidden" name="target" value="0" />
+                    <input type="hidden" name="effectivePid" value="' . htmlspecialchars((string)($this->data['effectivePid'] ?? '0')) . '" />
+                </typo3-t3editor-codemirror>';
+        } else {
+            $attributes['class'] = implode(' ', array_merge(explode(' ', $attributes['class']), ['formengine-textarea', 't3js-enable-tab']));
+            $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/json-element.js');
+            $editorHtml = '
+                <typo3-formengine-element-json recordFieldId="' . htmlspecialchars($fieldId) . '">
+                    <textarea ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . htmlspecialchars($itemValue) . '</textarea>
+                </typo3-formengine-element-json>';
+        }
+
+        $additionalHtml = [];
+        if (!$readOnly) {
+            $fieldControlResult = $this->renderFieldControl();
+            $fieldControlHtml = $fieldControlResult['html'];
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
+
+            if (!empty($fieldControlHtml)) {
+                $additionalHtml[] = '<div class="form-wizards-items-aside form-wizards-items-aside--field-control">';
+                $additionalHtml[] =     '<div class="btn-group">';
+                $additionalHtml[] =         $fieldControlHtml;
+                $additionalHtml[] =     '</div>';
+                $additionalHtml[] = '</div>';
+            }
+
+            $fieldWizardResult = $this->renderFieldWizard();
+            $fieldWizardHtml = $fieldWizardResult['html'];
+            $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
+
+            if (!empty($fieldWizardHtml)) {
+                $additionalHtml[] = '<div class="form-wizards-items-bottom">';
+                $additionalHtml[] =     $fieldWizardHtml;
+                $additionalHtml[] = '</div>';
+            }
+        }
+
+        $html = [];
+        $html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
+        $html[] =   $fieldInformationHtml;
+        $html[] =   '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px">' : '>');
+        $html[] =       '<div class="form-wizards-wrap">';
+        $html[] =           '<div class="form-wizards-element">';
+        $html[] =               $editorHtml;
+        $html[] =           '</div>';
+        $html[] =           implode(LF, $additionalHtml);
+        $html[] =       '</div>';
+        $html[] =   '</div>';
+        $html[] = '</div>';
+
+        $resultArray['html'] = implode(LF, $html);
+
+        return $resultArray;
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaColumnsProcessPlaceholders.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaColumnsProcessPlaceholders.php
index d050dbfe61b8..fb98cfda35cc 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaColumnsProcessPlaceholders.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaColumnsProcessPlaceholders.php
@@ -44,6 +44,7 @@ class TcaColumnsProcessPlaceholders implements FormDataProviderInterface
                     && $fieldConfig['config']['type'] !== 'password'
                     && $fieldConfig['config']['type'] !== 'datetime'
                     && $fieldConfig['config']['type'] !== 'color'
+                    && $fieldConfig['config']['type'] !== 'json'
                 )
             ) {
                 continue;
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
index c6d64e9fd6e0..8075737399f0 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInputPlaceholders.php
@@ -51,6 +51,7 @@ class TcaInputPlaceholders implements FormDataProviderInterface
                     && $fieldConfig['config']['type'] !== 'password'
                     && $fieldConfig['config']['type'] !== 'datetime'
                     && $fieldConfig['config']['type'] !== 'color'
+                    && $fieldConfig['config']['type'] !== 'json'
                 )
             ) {
                 continue;
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaJson.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaJson.php
new file mode 100644
index 000000000000..a222480ddf82
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaJson.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Form\FormDataProvider;
+
+use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
+
+/**
+ * Resolve and prepare json data.
+ */
+class TcaJson extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
+{
+    public function addData(array $result): array
+    {
+        // Currently only new records are considered
+        if ($result['command'] !== 'new') {
+            return $result;
+        }
+
+        foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
+            if (($fieldConfig['config']['type'] ?? '') !== 'json') {
+                continue;
+            }
+
+            // Ensure that even for new records, the field is always an array - especially if a default value is defined
+            if (is_string($result['databaseRow'][$fieldName])) {
+                try {
+                    $result['databaseRow'][$fieldName] = json_decode($result['databaseRow'][$fieldName], true, 512, JSON_THROW_ON_ERROR);
+                } catch (\JsonException) {
+                    $result['databaseRow'][$fieldName] = [];
+                }
+            }
+        }
+
+        return $result;
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php
index 86ce0a6f0ea4..273964dff896 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php
@@ -193,6 +193,9 @@ class TcaRecordTitle implements FormDataProviderInterface
                 break;
             case 'flex':
                 // @todo: Check if and how a label could be generated from flex field data
+                break;
+            case 'json':
+                // @todo: Check if and how a label could be generated from json field data
             default:
         }
 
diff --git a/typo3/sysext/backend/Classes/Form/NodeFactory.php b/typo3/sysext/backend/Classes/Form/NodeFactory.php
index e0f88fb7e801..069f8f5949a1 100644
--- a/typo3/sysext/backend/Classes/Form/NodeFactory.php
+++ b/typo3/sysext/backend/Classes/Form/NodeFactory.php
@@ -107,6 +107,7 @@ class NodeFactory
         'category' => Element\CategoryElement::class,
         'passthrough' => Element\PassThroughElement::class,
         'belayoutwizard' => Element\BackendLayoutWizardElement::class,
+        'json' => Element\JsonElement::class,
 
         // Default classes to enrich single elements
         'fieldControl' => NodeExpansion\FieldControl::class,
diff --git a/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php b/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php
index c18586f89c62..fc8b3715fd19 100644
--- a/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php
+++ b/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php
@@ -52,6 +52,7 @@ class FormEngineUtility
         'datetime' => ['size', 'readOnly'],
         'color' => ['size', 'readOnly'],
         'text' => ['cols', 'rows', 'wrap', 'max', 'readOnly'],
+        'json' => ['cols', 'rows', 'readOnly'],
         'check' => ['cols', 'readOnly'],
         'select' => ['size', 'autoSizeMax', 'maxitems', 'minitems', 'readOnly', 'treeConfig', 'fileFolderConfig'],
         'category' => ['size', 'maxitems', 'minitems', 'readOnly', 'treeConfig'],
diff --git a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
index 4f496e2dfe3f..245a4be3bbc7 100644
--- a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
+++ b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php
@@ -2391,6 +2391,7 @@ class DatabaseRecordList
                     }
                 } elseif ($fieldType === 'input'
                     || $fieldType === 'text'
+                    || $fieldType === 'json'
                     || $fieldType === 'flex'
                     || $fieldType === 'email'
                     || $fieldType === 'link'
@@ -2435,6 +2436,7 @@ class DatabaseRecordList
                 }
                 if ($fieldType === 'input'
                     || $fieldType === 'text'
+                    || $fieldType === 'json'
                     || $fieldType === 'flex'
                     || $fieldType === 'email'
                     || $fieldType === 'link'
diff --git a/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php b/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php
index 545d12539736..0b25202935e5 100644
--- a/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php
+++ b/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php
@@ -340,6 +340,7 @@ final class DatabaseRecordProvider implements SearchProviderInterface
         $searchableFieldTypes = [
             'input',
             'text',
+            'json',
             'flex',
             'email',
             'link',
diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
index 30435683019a..841a6bfe7550 100644
--- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php
+++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
@@ -1544,17 +1544,6 @@ class BackendUtility
             GeneralUtility::callUserFunction($_funcRef, $theColConf, $referenceObject);
         }
 
-        // For database type "JSON" the value in decoded state is most likely an array. This is not compatible with
-        // the "human-readable" processing and returning promise of this method. Thus, we ensure to handle value for
-        // this field as json encoded string. This should be the best readable version of the value data.
-        if ((string)($theColConf['dbType'] ?? '') === 'json'
-            && ((is_string($value) && !str_starts_with($value, '{') && !str_starts_with($value, '['))
-                || !is_string($value))
-        ) {
-            // @todo Consider to pretty print the json value, as this would match the "human readable" goal.
-            $value = \json_encode($value);
-        }
-
         $l = '';
         $lang = static::getLanguageService();
         switch ((string)($theColConf['type'] ?? '')) {
@@ -1727,6 +1716,22 @@ class BackendUtility
                     }
                 }
                 break;
+            case 'json':
+                // For database type "JSON" the value in decoded state is most likely an array. This is not compatible with
+                // the "human-readable" processing and returning promise of this method. Thus, we ensure to handle value for
+                // this field as json encoded string. This should be the best readable version of the value data.
+                if (
+                    (
+                        is_string($value)
+                        && !str_starts_with($value, '{')
+                        && !str_starts_with($value, '[')
+                    )
+                    || !is_string($value)
+                ) {
+                    // @todo Consider to pretty print the json value, as this would match the "human readable" goal.
+                    $value = json_encode($value);
+                }
+                // no break intended.
             default:
                 if ($defaultPassthrough) {
                     $l = $value;
@@ -3461,17 +3466,14 @@ class BackendUtility
     public static function convertDatabaseRowValuesToPhp(string $table, array $row): array
     {
         $tableTca = $GLOBALS['TCA'][$table] ?? [];
-        if (!$tableTca) {
+        if (!is_array($tableTca) || $tableTca === []) {
             return $row;
         }
         $platform = static::getConnectionForTable($table)->getDatabasePlatform();
         foreach ($row as $field => $value) {
-            if (($tableTca['columns'][$field]['config']['type'] ?? '') === 'user') {
-                $dbType = $GLOBALS['TCA'][$table]['columns'][$field]['config']['dbType'] ?? '';
-                // @todo Only handle a specific dbType for now.
-                if ($dbType === 'json') {
-                    $row[$field] = Type::getType($dbType)->convertToPHPValue($value, $platform);
-                }
+            // @todo Only handle specific TCA type=json
+            if (($tableTca['columns'][$field]['config']['type'] ?? '') === 'json') {
+                $row[$field] = Type::getType('json')->convertToPHPValue($value, $platform);
             }
         }
         return $row;
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/form-engine/element/json-element.js b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine/element/json-element.js
new file mode 100644
index 000000000000..17bd3e435a36
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine/element/json-element.js
@@ -0,0 +1,13 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+import{Resizable}from"@typo3/backend/form-engine/element/modifier/resizable.js";import{Tabbable}from"@typo3/backend/form-engine/element/modifier/tabbable.js";class JsonElement extends HTMLElement{constructor(){super(...arguments),this.element=null}connectedCallback(){this.element=document.getElementById(this.getAttribute("recordFieldId")||""),this.element&&(Resizable.enable(this.element),Tabbable.enable(this.element))}}window.customElements.define("typo3-formengine-element-json",JsonElement);
\ No newline at end of file
diff --git a/typo3/sysext/backend/Tests/Unit/Form/Element/JsonElementTest.php b/typo3/sysext/backend/Tests/Unit/Form/Element/JsonElementTest.php
new file mode 100644
index 000000000000..486f6b2e6f45
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Unit/Form/Element/JsonElementTest.php
@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Tests\Unit\Form\Element;
+
+use TYPO3\CMS\Backend\Form\Element\JsonElement;
+use TYPO3\CMS\Backend\Form\NodeExpansion\FieldInformation;
+use TYPO3\CMS\Backend\Form\NodeFactory;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\T3editor\Mode;
+use TYPO3\CMS\T3editor\Registry\ModeRegistry;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class JsonElementTest extends UnitTestCase
+{
+    protected bool $resetSingletonInstances = true;
+
+    /**
+     * @test
+     */
+    public function renderReturnsJsonInStandardTextarea(): void
+    {
+        $data = [
+                'parameterArray' => [
+                'itemFormElName' => 'config',
+                'itemFormElValue' => ['foo' => 'bar'],
+                'fieldConf' => [
+                    'config' => [
+                        'type' => 'json',
+                        'enableCodeEditor' => false,
+                        'placeholder' => 'placeholder',
+                    ],
+                ],
+            ],
+        ];
+
+        GeneralUtility::addInstance(IconFactory::class, $this->createMock(IconFactory::class));
+
+        $nodeFactoryMock = $this->createMock(NodeFactory::class);
+        $fieldInformationMock = $this->createMock(FieldInformation::class);
+        $fieldInformationMock->method('render')->willReturn(['html' => '']);
+        $nodeFactoryMock->method('create')->with(self::anything())->willReturn($fieldInformationMock);
+
+        $subject = new JsonElement($nodeFactoryMock, $data);
+        $result = $subject->render();
+
+        self::assertEquals('@typo3/backend/form-engine/element/json-element.js', $result['javaScriptModules'][0]->getName());
+        self::assertStringContainsString('<typo3-formengine-element-json', $result['html']);
+        self::assertStringContainsString('placeholder="placeholder"', $result['html']);
+        self::assertStringContainsString('&quot;foo&quot;: &quot;bar&quot;', $result['html']);
+    }
+
+    /**
+     * @test
+     */
+    public function renderReturnsJsonInCodeEditor(): void
+    {
+        $data = [
+            'tableName' => 'aTable',
+            'fieldName' => 'aField',
+            'parameterArray' => [
+                'itemFormElName' => 'config',
+                'itemFormElValue' => ['foo' => 'bar'],
+                'fieldConf' => [
+                    'config' => [
+                        'type' => 'json',
+                        'placeholder' => 'placeholder',
+                    ],
+                ],
+            ],
+        ];
+
+        GeneralUtility::addInstance(IconFactory::class, $this->createMock(IconFactory::class));
+        GeneralUtility::setSingletonInstance(PackageManager::class, $this->createMock(PackageManager::class));
+
+        $cacheManagerMock = $this->createMock(CacheManager::class);
+        $cacheMock = $this->createMock(FrontendInterface::class);
+        $cacheManagerMock->method('getCache')->with('assets')->willReturn($cacheMock);
+        $cacheMock->method('get')->withAnyParameters()->willReturn([]);
+        GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerMock);
+
+        $modeRegistryMock = $this->createMock(ModeRegistry::class);
+        $modeRegistryMock->method('getDefaultMode')->willReturn(new Mode(JavaScriptModuleInstruction::create('foo')));
+        GeneralUtility::setSingletonInstance(ModeRegistry::class, $modeRegistryMock);
+
+        $nodeFactoryMock = $this->createMock(NodeFactory::class);
+        $fieldInformationMock = $this->createMock(FieldInformation::class);
+        $fieldInformationMock->method('render')->willReturn(['html' => '']);
+        $nodeFactoryMock->method('create')->with(self::anything())->willReturn($fieldInformationMock);
+
+        $subject = new JsonElement($nodeFactoryMock, $data);
+        $result = $subject->render();
+
+        self::assertEquals('@typo3/t3editor/element/code-mirror-element.js', $result['javaScriptModules'][0]->getName());
+        self::assertStringContainsString('<typo3-t3editor-codemirror', $result['html']);
+        self::assertStringContainsString('placeholder="placeholder"', $result['html']);
+        self::assertStringContainsString('&quot;foo&quot;: &quot;bar&quot;', $result['html']);
+    }
+}
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaJsonTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaJsonTest.php
new file mode 100644
index 000000000000..e464fdb661b3
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaJsonTest.php
@@ -0,0 +1,241 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider;
+
+use TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class TcaJsonTest extends UnitTestCase
+{
+    public function resultArrayDataProvider(): \Generator
+    {
+        yield 'Only handle new records' => [
+            [
+                'command' => 'edit',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => '{"foo":"bar"}',
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            [
+                'command' => 'edit',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => '{"foo":"bar"}',
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        yield 'Only handle TCA type "json" records' => [
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => '{"foo":"bar"}',
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'text',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => '{"foo":"bar"}',
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'text',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        yield 'Only handles string values' => [
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => ['foo' => 'bar'],
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => ['foo' => 'bar'],
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        yield 'String values are properly decoded' => [
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => '{"foo":"bar"}',
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => ['foo' => 'bar'],
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        yield 'Invalid values are handled properly' => [
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => '_-invalid-_',
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => [],
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        yield 'Initialize empty values' => [
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => '',
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            [
+                'command' => 'new',
+                'tableName' => 'aTable',
+                'databaseRow' => [
+                    'aField' => [],
+                ],
+                'processedTca' => [
+                    'columns' => [
+                        'aField' => [
+                            'config' => [
+                                'type' => 'json',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider resultArrayDataProvider
+     */
+    public function addDataDoesHandleJsonRecords(array $input, array $expected): void
+    {
+        self::assertSame($expected, (new TcaJson())->addData($input));
+    }
+}
diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index 29719bdc50f2..e5e1b0c41498 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -1502,6 +1502,7 @@ class DataHandler implements LoggerAwareInterface
             'slug' => $this->checkValueForSlug((string)$value, $tcaFieldConf, $table, $id, (int)$realPid, $field, $additionalData['incomingFieldArray'] ?? []),
             'text' => $this->checkValueForText($value, $tcaFieldConf, $table, $realPid, $field),
             'group', 'folder', 'select' => $this->checkValueForGroupFolderSelect($res, $value, $tcaFieldConf, $table, $id, $status, $field),
+            'json' => $this->checkValueForJson($value, $tcaFieldConf),
             'passthrough', 'imageManipulation', 'user' => ['value' => $value],
             default => [],
         };
@@ -2268,6 +2269,42 @@ class DataHandler implements LoggerAwareInterface
         return $res;
     }
 
+    /**
+     * Evaluate "json" type values.
+     *
+     * @param array|string $value The value to set.
+     * @param array $tcaFieldConf Field configuration from TCA
+     * @return array The result array. The processed value (if any!) is set in the "value" key.
+     */
+    protected function checkValueForJson(array|string $value, array $tcaFieldConf): array
+    {
+        if (is_string($value)) {
+            if ($value === '') {
+                $value = [];
+            } else {
+                try {
+                    $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
+                    if ($value === null) {
+                        // Unset value as it could not be decoded
+                        return [];
+                    }
+                } catch (\JsonException) {
+                    // Unset value as it is invalid
+                    return [];
+                }
+            }
+        }
+
+        if (!$this->validateValueForRequired($tcaFieldConf, $value)) {
+            // Unset value as it is required
+            return [];
+        }
+
+        return [
+            'value' => $value,
+        ];
+    }
+
     /**
      * Evaluates 'group', 'folder' or 'select' type values.
      *
@@ -7701,13 +7738,10 @@ class DataHandler implements LoggerAwareInterface
                 // Traverse array of values that was inserted into the database and compare with the actually stored value:
                 $errors = [];
                 foreach ($fieldArray as $key => $value) {
-                    $tcaColumnConfig = $tcaTableColumns[$key]['config'] ?? [];
-                    $columnType = $tcaColumnConfig['type'] ?? '';
-                    $columnDbType = $tcaColumnConfig['dbType'] ?? '';
                     if (!$this->checkStoredRecords_loose || $value || $row[$key]) {
                         // @todo Check explicitly for one type is fishy. However needed to avoid array to string
                         //       conversion errors. Find a better way do handle this.
-                        if ($columnType === 'user' && $columnDbType === 'json') {
+                        if (($tcaTableColumns[$key]['config']['type'] ?? '') === 'json') {
                             // To ensure a proper comparison we need to sort the array structure based on array keys
                             // in a recursive manner. Otherwise, we would emit an error just because the ordering was
                             // different. This must be done for value and the value in the row to be safe.
diff --git a/typo3/sysext/core/Classes/DataHandling/TableColumnType.php b/typo3/sysext/core/Classes/DataHandling/TableColumnType.php
index decd1894a050..be263688ba13 100644
--- a/typo3/sysext/core/Classes/DataHandling/TableColumnType.php
+++ b/typo3/sysext/core/Classes/DataHandling/TableColumnType.php
@@ -50,6 +50,7 @@ final class TableColumnType extends Enumeration
     public const COLOR = 'COLOR';
     public const NUMBER = 'NUMBER';
     public const FILE = 'FILE';
+    public const JSON = 'JSON';
 
     /**
      * @param mixed $type
diff --git a/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php b/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php
index 42d54f703622..e22d2838726a 100644
--- a/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php
+++ b/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php
@@ -422,87 +422,105 @@ class DefaultTcaSchema
                 $tables[$tablePosition]->addIndex(['t3ver_oid', 't3ver_wsid'], 't3ver_oid');
             }
 
+            // In the following, columns for TCA fields with a dedicated TCA type are
+            // added. In the unlikely case that no columns exist, we can skip the table.
+            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
+                continue;
+            }
+
             // Add category fields for all tables, defining category columns (TCA type=category)
-            if (isset($tableDefinition['columns']) && is_array($tableDefinition['columns'])) {
-                foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
-                    if ((string)($fieldConfig['config']['type'] ?? '') !== 'category'
-                        || $this->isColumnDefinedForTable($tables, $tableName, $fieldName)
-                    ) {
-                        continue;
-                    }
+            foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
+                if ((string)($fieldConfig['config']['type'] ?? '') !== 'category'
+                    || $this->isColumnDefinedForTable($tables, $tableName, $fieldName)
+                ) {
+                    continue;
+                }
 
-                    if (($fieldConfig['config']['relationship'] ?? '') === 'oneToMany') {
-                        $tables[$tablePosition]->addColumn(
-                            $this->quote($fieldName),
-                            'text',
-                            [
-                                'notnull' => false,
-                            ]
-                        );
-                    } else {
-                        $tables[$tablePosition]->addColumn(
-                            $this->quote($fieldName),
-                            'integer',
-                            [
-                                'default' => 0,
-                                'notnull' => true,
-                                'unsigned' => true,
-                            ]
-                        );
-                    }
+                if (($fieldConfig['config']['relationship'] ?? '') === 'oneToMany') {
+                    $tables[$tablePosition]->addColumn(
+                        $this->quote($fieldName),
+                        'text',
+                        [
+                            'notnull' => false,
+                        ]
+                    );
+                } else {
+                    $tables[$tablePosition]->addColumn(
+                        $this->quote($fieldName),
+                        'integer',
+                        [
+                            'default' => 0,
+                            'notnull' => true,
+                            'unsigned' => true,
+                        ]
+                    );
                 }
             }
 
             // Add datetime fields for all tables, defining datetime columns (TCA type=datetime), except
             // those columns, which had already been added due to definition in "ctrl", e.g. "starttime".
-            if (isset($tableDefinition['columns']) && is_array($tableDefinition['columns'])) {
-                foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
-                    if ((string)($fieldConfig['config']['type'] ?? '') !== 'datetime'
-                        || $this->isColumnDefinedForTable($tables, $tableName, $fieldName)
-                    ) {
-                        continue;
-                    }
-
-                    if (in_array($fieldConfig['config']['dbType'] ?? '', QueryHelper::getDateTimeTypes(), true)) {
-                        $tables[$tablePosition]->addColumn(
-                            $this->quote($fieldName),
-                            $fieldConfig['config']['dbType'],
-                            [
-                                'notnull' => false,
-                            ]
-                        );
-                    } else {
-                        $tables[$tablePosition]->addColumn(
-                            $this->quote($fieldName),
-                            'integer',
-                            [
-                                'default' => 0,
-                                'notnull' => !($fieldConfig['config']['nullable'] ?? false),
-                                'unsigned' => false,
-                            ]
-                        );
-                    }
+            foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
+                if ((string)($fieldConfig['config']['type'] ?? '') !== 'datetime'
+                    || $this->isColumnDefinedForTable($tables, $tableName, $fieldName)
+                ) {
+                    continue;
                 }
-            }
-
-            // Add slug fields for all tables, defining slug columns (TCA type=slug)
-            if (isset($tableDefinition['columns']) && is_array($tableDefinition['columns'])) {
-                foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
-                    if ((string)($fieldConfig['config']['type'] ?? '') !== 'slug'
-                        || $this->isColumnDefinedForTable($tables, $tableName, $fieldName)
-                    ) {
-                        continue;
-                    }
 
+                if (in_array($fieldConfig['config']['dbType'] ?? '', QueryHelper::getDateTimeTypes(), true)) {
                     $tables[$tablePosition]->addColumn(
                         $this->quote($fieldName),
-                        'string',
+                        $fieldConfig['config']['dbType'],
                         [
-                            'length' => 2048,
                             'notnull' => false,
                         ]
                     );
+                } else {
+                    $tables[$tablePosition]->addColumn(
+                        $this->quote($fieldName),
+                        'integer',
+                        [
+                            'default' => 0,
+                            'notnull' => !($fieldConfig['config']['nullable'] ?? false),
+                            'unsigned' => false,
+                        ]
+                    );
+                }
+            }
+
+            // Add slug fields for all tables, defining slug columns (TCA type=slug)
+            foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
+                if ((string)($fieldConfig['config']['type'] ?? '') !== 'slug'
+                    || $this->isColumnDefinedForTable($tables, $tableName, $fieldName)
+                ) {
+                    continue;
+                }
+
+                $tables[$tablePosition]->addColumn(
+                    $this->quote($fieldName),
+                    'string',
+                    [
+                        'length' => 2048,
+                        'notnull' => false,
+                    ]
+                );
+            }
+
+            // Add json fields for all tables, defining json columns (TCA type=json)
+            foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
+                if ((string)($fieldConfig['config']['type'] ?? '') !== 'json'
+                    || $this->isColumnDefinedForTable($tables, $tableName, $fieldName)
+                ) {
+                    continue;
                 }
+
+                $tables[$tablePosition]->addColumn(
+                    $this->quote($fieldName),
+                    'json',
+                    [
+                        'default' => '[]',
+                        'notnull' => true,
+                    ]
+                );
             }
         }
 
diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php
index 46170355d4e8..8dcc458a1962 100644
--- a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php
+++ b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php
@@ -108,6 +108,7 @@ class SearchTermRestriction implements QueryRestrictionInterface
                 // Assemble the search condition only if the field makes sense to be searched
                 if ($fieldType === 'text'
                     || $fieldType === 'flex'
+                    || $fieldType === 'json'
                     || $fieldType === 'email'
                     || $fieldType === 'link'
                     || $fieldType === 'color'
diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index e73ed8e7b6f2..0e6a4e8620f3 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -597,10 +597,15 @@ return [
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaFlexProcess::class,
                         ],
                     ],
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class => [
+                        'depends' => [
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class,
+                        ],
+                    ],
                     \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class => [
                         'depends' => [
                             \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class,
-                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class,
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class,
                         ],
                     ],
                     \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class => [
@@ -842,6 +847,11 @@ return [
                             \TYPO3\CMS\Backend\Form\FormDataProvider\SiteResolving::class,
                         ],
                     ],
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class => [
+                        'depends' => [
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class,
+                        ],
+                    ],
                     \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class => [
                         'depends' => [
                             \TYPO3\CMS\Backend\Form\FormDataProvider\SiteResolving::class,
@@ -927,11 +937,16 @@ return [
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsRemoveUnused::class,
                         ],
                     ],
-                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class => [
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class => [
                         'depends' => [
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class,
                         ],
                     ],
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class => [
+                        'depends' => [
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class,
+                        ],
+                    ],
                     \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class => [
                         'depends' => [
                             \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsRemoveUnused::class,
diff --git a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99226-IntroduceDbTypeJsonForTCATypeUser.rst b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99226-IntroduceDbTypeJsonForTCATypeUser.rst
index 0b9f44980db8..03f462263fad 100644
--- a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99226-IntroduceDbTypeJsonForTCATypeUser.rst
+++ b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-99226-IntroduceDbTypeJsonForTCATypeUser.rst
@@ -8,6 +8,14 @@ Feature: #99226 - Introduce dbType json for TCA type user
 
 See :issue:`99226`
 
+
+.. attention::
+
+    This TCA option is **no longer available**! It has been
+    :ref:`replaced <important-100088-1677950866>` by the dedicated
+    :ref:`json <feature-100088-1677965005>` TCA type. Do not use
+    this option in your installation, but use the new TCA type.
+
 Description
 ===========
 
diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100088-NewTCATypeJson.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100088-NewTCATypeJson.rst
new file mode 100644
index 000000000000..6512c6d5a96a
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100088-NewTCATypeJson.rst
@@ -0,0 +1,58 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-100088-1677965005:
+
+======================================
+Feature: #100088 - New TCA type "json"
+======================================
+
+See :issue:`100088`
+
+Description
+===========
+
+In our effort of introducing dedicated TCA types for special use cases,
+a new TCA field type called :php:`json` has been added to TYPO3 Core.
+Its main purpose is to simplify the TCA configuration when working with
+fields, containing JSON data. It therefore :ref:`replaces <important-100088-1677950866>`
+the previously introduced :php:`dbtype=json` of TCA type :php:`user`.
+
+Using the new type, TYPO3 automatically takes care of adding the corresponding
+database column.
+
+The TCA type :php:`json` features the following column configuration:
+
+- :php:`behaviour`: :php:`allowLanguageSynchronization`
+- :php:`cols`
+- :php:`default`
+- :php:`enableCodeEditor`
+- :php:`fieldControl`
+- :php:`fieldInformation`
+- :php:`fieldWizard`
+- :php:`placeholder`
+- :php:`readOnly`
+- :php:`required`
+- :php:`rows`
+
+.. note::
+
+    In case :php:`enableCodeEditor` is set to :php:`true`, which is the default
+    and the system extension `t3editor` is installed and active, the JSON value
+    is rendered in the corresponding code editor. Otherwise it is rendered in a
+    standard `textarea` HTML element.
+
+The following column configuration can be overwritten by Page TSconfig:
+
+- :typoscript:`cols`
+- :typoscript:`rows`
+- :typoscript:`readOnly`
+
+
+
+Impact
+======
+
+It's now possible to use a dedicated TCA type for rendering of JSON fields.
+Using the new TCA type, corresponding database columns are added automatically.
+
+.. index:: Backend, PHP-API, TCA, ext:backend
diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Important-100088-RemoveDbTypeJsonForTCATypeUser.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Important-100088-RemoveDbTypeJsonForTCATypeUser.rst
new file mode 100644
index 000000000000..2da9b3e11962
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.3/Important-100088-RemoveDbTypeJsonForTCATypeUser.rst
@@ -0,0 +1,48 @@
+.. include:: /Includes.rst.txt
+
+.. _important-100088-1677950866:
+
+=========================================================
+Important: #100088 - Remove dbType json for TCA type user
+=========================================================
+
+See :issue:`100088`
+
+Description
+===========
+
+With :issue:`99226` the `dbType=json` option has been added for
+TCA type `user`. After some reconsideration, it has been decided
+to drop this option again in favor of the dedicated TCA type `json`.
+Have a look to the according :ref:`changelog <feature-100088-1677965005>`
+for further information.
+
+Since the `dbType` option has not been released in any LTS version yet,
+the option is dropped without further deprecation. Also no TCA migration
+is applied.
+
+In case you make already use of this `dbType` in your custom extension,
+you need to migrate to the new TCA type.
+
+Example:
+
+..  code-block:: php
+
+    // Before
+    'myField' => [
+        'config' => [
+            'type' => 'user',
+            'renderType' => 'myRenderType',
+            'dbType' => 'json',
+        ],
+    ],
+
+    // After
+    'myField' => [
+        'config' => [
+            'type' => 'json',
+            'renderType' => 'myRenderType',
+        ],
+    ],
+
+.. index:: Backend, PHP-API, TCA, ext:backend
diff --git a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
index 3a90eb3168e2..734abd345c94 100644
--- a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
+++ b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
@@ -1059,6 +1059,42 @@ class DataHandlerTest extends UnitTestCase
         self::assertSame($expectedResult, $this->subject->_call('checkValueForInput', null, ['type' => 'input', 'max' => 40], 'tt_content', 'NEW55c0e67f8f4d32.04974534', 89, 'table_caption'));
     }
 
+    public function checkValueForJsonDataProvider(): \Generator
+    {
+        yield 'Converts empty string to array' => [
+            '',
+            ['value' => []],
+        ];
+        yield 'Handles invalid JSON' => [
+            '_-invalid-_',
+            [],
+        ];
+        yield 'Decodes JSON' => [
+            '{"foo":"bar"}',
+            ['value' => ['foo' => 'bar']],
+        ];
+        yield 'Array is not decoded' => [
+            ['foo' => 'bar'],
+            ['value' => ['foo' => 'bar']],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider checkValueForJsonDataProvider
+     */
+    public function checkValueForJson(string|array $input, array $expected): void
+    {
+        self::assertSame(
+            $expected,
+            $this->subject->_call(
+                'checkValueForJson',
+                $input,
+                ['type' => 'json']
+            )
+        );
+    }
+
     /**
      * @test
      * @dataProvider referenceValuesAreCastedDataProvider
diff --git a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php
index aafc403c4ea4..a9d19203f5f8 100644
--- a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php
@@ -463,6 +463,7 @@ class DataMapFactoryTest extends UnitTestCase
             [['type' => 'color'], TableColumnType::COLOR],
             [['type' => 'number'], TableColumnType::NUMBER],
             [['type' => 'file'], TableColumnType::FILE],
+            [['type' => 'json'], TableColumnType::JSON],
         ];
     }
 
diff --git a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php
index b6154a126240..c919db762756 100644
--- a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php
+++ b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php
@@ -928,6 +928,7 @@ class DatabaseIntegrityController
                     case 'link':
                     case 'password':
                     case 'color':
+                    case 'json':
                     default:
                         $fields['type'] = 'text';
                 }
@@ -2264,6 +2265,7 @@ class DatabaseIntegrityController
                         case 'link':
                         case 'password':
                         case 'color':
+                        case 'json':
                         default:
                             $this->fields[$fieldName]['type'] = 'text';
                     }
diff --git a/typo3/sysext/reactions/Classes/Form/Element/FieldMapElement.php b/typo3/sysext/reactions/Classes/Form/Element/FieldMapElement.php
index 6475950e22ed..0f73eb0f810b 100644
--- a/typo3/sysext/reactions/Classes/Form/Element/FieldMapElement.php
+++ b/typo3/sysext/reactions/Classes/Form/Element/FieldMapElement.php
@@ -22,7 +22,7 @@ use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
 /**
  * Creates a dynamic element to add values to table fields.
  *
- * This is rendered for config type=user, renderType=fieldMap
+ * This is rendered for config type=json, renderType=fieldMap
  *
  * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
  */
diff --git a/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php b/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php
index c9adbb89e560..ec99b2b2677b 100644
--- a/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php
+++ b/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php
@@ -18,10 +18,8 @@
             'description' => 'LLL:EXT:reactions/Resources/Private/Language/locallang_db.xlf:sys_reaction.fields.description',
             'displayCond' => 'FIELD:table_name:REQ:true',
             'config' => [
-                'type' => 'user',
+                'type' => 'json',
                 'renderType' => 'fieldMap',
-                'dbType' => 'json',
-                'default' => '{}',
             ],
         ],
     ]
diff --git a/typo3/sysext/reactions/ext_tables.sql b/typo3/sysext/reactions/ext_tables.sql
index e235882ddc8d..87fc55ea9df2 100644
--- a/typo3/sysext/reactions/ext_tables.sql
+++ b/typo3/sysext/reactions/ext_tables.sql
@@ -9,7 +9,6 @@ CREATE TABLE sys_reaction (
 	impersonate_user int(11) unsigned DEFAULT '0' NOT NULL,
 	table_name varchar(255) DEFAULT '' NOT NULL,
 	storage_pid int(11) unsigned DEFAULT '0' NOT NULL,
-	fields json NOT NULL,
 
 	UNIQUE identifier_key (identifier),
 	KEY index_source (reaction_type(5))
diff --git a/typo3/sysext/t3editor/Configuration/Backend/T3editor/Modes.php b/typo3/sysext/t3editor/Configuration/Backend/T3editor/Modes.php
index ce387d59d109..8db36582ffc6 100644
--- a/typo3/sysext/t3editor/Configuration/Backend/T3editor/Modes.php
+++ b/typo3/sysext/t3editor/Configuration/Backend/T3editor/Modes.php
@@ -19,6 +19,10 @@ return [
         'module' => JavaScriptModuleInstruction::create('@codemirror/lang-javascript', 'javascript')->invoke(),
         'extensions' => ['javascript'],
     ],
+    'json' => [
+        'module' => JavaScriptModuleInstruction::create('@codemirror/lang-json', 'json')->invoke(),
+        'extensions' => ['json'],
+    ],
     'php' => [
         'module' => JavaScriptModuleInstruction::create('@codemirror/lang-php', 'php')->invoke(),
         'extensions' => ['php', 'php5', 'php7', 'phps'],
diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js
index 040021b701af..ed24d362c792 100644
--- a/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js
+++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/element/code-mirror-element.js
@@ -10,11 +10,11 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-var __decorate=function(e,t,o,r){var i,l=arguments.length,n=l<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(e,t,o,r);else for(var a=e.length-1;a>=0;a--)(i=e[a])&&(n=(l<3?i(n):l>3?i(t,o,n):i(t,o))||n);return l>3&&n&&Object.defineProperty(t,o,n),n};import{LitElement,html,css}from"lit";import{customElement,property,state}from"lit/decorators.js";import{EditorView,lineNumbers,highlightSpecialChars,drawSelection,keymap}from"@codemirror/view";import{EditorState}from"@codemirror/state";import{syntaxHighlighting,defaultHighlightStyle}from"@codemirror/language";import{defaultKeymap,indentWithTab}from"@codemirror/commands";import{oneDark}from"@codemirror/theme-one-dark";import{executeJavaScriptModuleInstruction,loadModule,resolveSubjectRef}from"@typo3/core/java-script-item-processor.js";import"@typo3/backend/element/spinner-element.js";let CodeMirrorElement=class extends LitElement{constructor(){super(...arguments),this.mode=null,this.addons=[],this.keymaps=[],this.lineDigits=0,this.autoheight=!1,this.nolazyload=!1,this.readonly=!1,this.fullscreen=!1,this.panel="bottom",this.editorView=null}render(){return html`
+var __decorate=function(e,t,r,o){var i,l=arguments.length,n=l<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,r):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(e,t,r,o);else for(var a=e.length-1;a>=0;a--)(i=e[a])&&(n=(l<3?i(n):l>3?i(t,r,n):i(t,r))||n);return l>3&&n&&Object.defineProperty(t,r,n),n};import{LitElement,html,css}from"lit";import{customElement,property,state}from"lit/decorators.js";import{EditorView,lineNumbers,highlightSpecialChars,drawSelection,keymap,placeholder}from"@codemirror/view";import{EditorState}from"@codemirror/state";import{syntaxHighlighting,defaultHighlightStyle}from"@codemirror/language";import{defaultKeymap,indentWithTab}from"@codemirror/commands";import{oneDark}from"@codemirror/theme-one-dark";import{executeJavaScriptModuleInstruction,loadModule,resolveSubjectRef}from"@typo3/core/java-script-item-processor.js";import"@typo3/backend/element/spinner-element.js";let CodeMirrorElement=class extends LitElement{constructor(){super(...arguments),this.mode=null,this.addons=[],this.keymaps=[],this.lineDigits=0,this.autoheight=!1,this.nolazyload=!1,this.readonly=!1,this.fullscreen=!1,this.panel="bottom",this.editorView=null}render(){return html`
       <div id="codemirror-parent" @keydown=${e=>this.onKeydown(e)}></div>
       ${this.label?html`<div class="panel panel-${this.panel}">${this.label}</div>`:""}
       ${null===this.editorView?html`<typo3-backend-spinner size="large" variant="dark"></typo3-backend-spinner>`:""}
-    `}firstUpdated(){if(this.nolazyload)return void this.initializeEditor(this.firstElementChild);const e={root:document.body};let t=new IntersectionObserver((e=>{e.forEach((e=>{e.intersectionRatio>0&&(t.unobserve(e.target),this.firstElementChild&&"textarea"===this.firstElementChild.nodeName.toLowerCase()&&this.initializeEditor(this.firstElementChild))}))}),e);t.observe(this)}onKeydown(e){e.ctrlKey&&e.altKey&&"f"===e.key&&(e.preventDefault(),this.fullscreen=!0),"Escape"===e.key&&this.fullscreen&&(e.preventDefault(),this.fullscreen=!1)}async initializeEditor(e){const t=EditorView.updateListener.of((t=>{t.docChanged&&(e.value=t.state.doc.toString(),e.dispatchEvent(new CustomEvent("change",{bubbles:!0})))}));this.lineDigits>0?this.style.setProperty("--rows",this.lineDigits.toString()):e.getAttribute("rows")&&this.style.setProperty("--rows",e.getAttribute("rows"));const o=[oneDark,t,lineNumbers(),highlightSpecialChars(),drawSelection(),EditorState.allowMultipleSelections.of(!0),syntaxHighlighting(defaultHighlightStyle,{fallback:!0})];if(this.readonly&&o.push(EditorState.readOnly.of(!0)),this.mode){const e=await executeJavaScriptModuleInstruction(this.mode);o.push(...e)}this.addons.length>0&&o.push(...await Promise.all(this.addons.map((e=>executeJavaScriptModuleInstruction(e)))));const r=[...defaultKeymap,indentWithTab];if(this.keymaps.length>0){const e=await Promise.all(this.keymaps.map((e=>loadModule(e).then((t=>resolveSubjectRef(t,e))))));e.forEach((e=>r.push(...e)))}o.push(keymap.of(r)),this.editorView=new EditorView({state:EditorState.create({doc:e.value,extensions:o}),parent:this.renderRoot.querySelector("#codemirror-parent"),root:this.renderRoot})}};CodeMirrorElement.styles=css`
+    `}firstUpdated(){if(this.nolazyload)return void this.initializeEditor(this.firstElementChild);const e={root:document.body};let t=new IntersectionObserver((e=>{e.forEach((e=>{e.intersectionRatio>0&&(t.unobserve(e.target),this.firstElementChild&&"textarea"===this.firstElementChild.nodeName.toLowerCase()&&this.initializeEditor(this.firstElementChild))}))}),e);t.observe(this)}onKeydown(e){e.ctrlKey&&e.altKey&&"f"===e.key&&(e.preventDefault(),this.fullscreen=!0),"Escape"===e.key&&this.fullscreen&&(e.preventDefault(),this.fullscreen=!1)}async initializeEditor(e){const t=EditorView.updateListener.of((t=>{t.docChanged&&(e.value=t.state.doc.toString(),e.dispatchEvent(new CustomEvent("change",{bubbles:!0})))}));this.lineDigits>0?this.style.setProperty("--rows",this.lineDigits.toString()):e.getAttribute("rows")&&this.style.setProperty("--rows",e.getAttribute("rows"));const r=[oneDark,t,lineNumbers(),highlightSpecialChars(),drawSelection(),EditorState.allowMultipleSelections.of(!0),syntaxHighlighting(defaultHighlightStyle,{fallback:!0})];if(this.readonly&&r.push(EditorState.readOnly.of(!0)),this.placeholder&&r.push(placeholder(this.placeholder)),this.mode){const e=await executeJavaScriptModuleInstruction(this.mode);r.push(...e)}this.addons.length>0&&r.push(...await Promise.all(this.addons.map((e=>executeJavaScriptModuleInstruction(e)))));const o=[...defaultKeymap,indentWithTab];if(this.keymaps.length>0){const e=await Promise.all(this.keymaps.map((e=>loadModule(e).then((t=>resolveSubjectRef(t,e))))));e.forEach((e=>o.push(...e)))}r.push(keymap.of(o)),this.editorView=new EditorView({state:EditorState.create({doc:e.value,extensions:r}),parent:this.renderRoot.querySelector("#codemirror-parent"),root:this.renderRoot})}};CodeMirrorElement.styles=css`
     :host {
       display: flex;
       flex-direction: column;
@@ -77,4 +77,4 @@ var __decorate=function(e,t,o,r){var i,l=arguments.length,n=l<3?t:null===r?r=Obj
       border-bottom-width: 1px;
       order: -1;
     }
-  `,__decorate([property({type:Object})],CodeMirrorElement.prototype,"mode",void 0),__decorate([property({type:Array})],CodeMirrorElement.prototype,"addons",void 0),__decorate([property({type:Array})],CodeMirrorElement.prototype,"keymaps",void 0),__decorate([property({type:Number})],CodeMirrorElement.prototype,"lineDigits",void 0),__decorate([property({type:Boolean,reflect:!0})],CodeMirrorElement.prototype,"autoheight",void 0),__decorate([property({type:Boolean})],CodeMirrorElement.prototype,"nolazyload",void 0),__decorate([property({type:Boolean})],CodeMirrorElement.prototype,"readonly",void 0),__decorate([property({type:Boolean,reflect:!0})],CodeMirrorElement.prototype,"fullscreen",void 0),__decorate([property({type:String})],CodeMirrorElement.prototype,"label",void 0),__decorate([property({type:String})],CodeMirrorElement.prototype,"panel",void 0),__decorate([state()],CodeMirrorElement.prototype,"editorView",void 0),CodeMirrorElement=__decorate([customElement("typo3-t3editor-codemirror")],CodeMirrorElement);export{CodeMirrorElement};
\ No newline at end of file
+  `,__decorate([property({type:Object})],CodeMirrorElement.prototype,"mode",void 0),__decorate([property({type:Array})],CodeMirrorElement.prototype,"addons",void 0),__decorate([property({type:Array})],CodeMirrorElement.prototype,"keymaps",void 0),__decorate([property({type:Number})],CodeMirrorElement.prototype,"lineDigits",void 0),__decorate([property({type:Boolean,reflect:!0})],CodeMirrorElement.prototype,"autoheight",void 0),__decorate([property({type:Boolean})],CodeMirrorElement.prototype,"nolazyload",void 0),__decorate([property({type:Boolean})],CodeMirrorElement.prototype,"readonly",void 0),__decorate([property({type:Boolean,reflect:!0})],CodeMirrorElement.prototype,"fullscreen",void 0),__decorate([property({type:String})],CodeMirrorElement.prototype,"label",void 0),__decorate([property({type:String})],CodeMirrorElement.prototype,"placeholder",void 0),__decorate([property({type:String})],CodeMirrorElement.prototype,"panel",void 0),__decorate([state()],CodeMirrorElement.prototype,"editorView",void 0),CodeMirrorElement=__decorate([customElement("typo3-t3editor-codemirror")],CodeMirrorElement);export{CodeMirrorElement};
\ No newline at end of file
-- 
GitLab