From 5c5f4b1a1f54793697df03b656b098329903c43c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 17:22:29 +0000 Subject: [PATCH 1/3] Initial plan From be3f058096703913cc25ffe4d5b263fdf01878f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 17:29:16 +0000 Subject: [PATCH 2/3] Detect legacy HTML in content_builder and offer migration to slices --- assets/content-builder.css | 21 +++ assets/content-builder.js | 123 ++++++++++++++++++ lang/de_de.lang | 9 ++ lang/en_gb.lang | 9 ++ lib/rex_yform_value_content_builder.php | 50 ++++++- pages/settings.php | 37 ++++++ .../bootstrap/value.content_builder.tpl.php | 43 +++++- 7 files changed, 288 insertions(+), 4 deletions(-) diff --git a/assets/content-builder.css b/assets/content-builder.css index c66694b..a758535 100644 --- a/assets/content-builder.css +++ b/assets/content-builder.css @@ -1381,3 +1381,24 @@ body.modal-open { overflow-wrap: break-word; text-align: left; } + +/* Legacy HTML Migration */ +.yform-content-builder .content-builder-legacy { + margin-bottom: 15px; +} + +.yform-content-builder .content-builder-legacy-notice { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.yform-content-builder .content-builder-legacy-notice-text { + flex: 1 1 auto; +} + +.yform-content-builder .content-builder-legacy-notice .content-builder-legacy-migrate { + flex: 0 0 auto; + margin-left: auto; +} diff --git a/assets/content-builder.js b/assets/content-builder.js index aafe329..ba6d36d 100644 --- a/assets/content-builder.js +++ b/assets/content-builder.js @@ -32,6 +32,33 @@ this.initMoveButtons(); this.initGridViews(); this.updateSectionClasses(); + this.initLegacyEditors(); + }, + + /** + * Legacy-HTML-Editor initialisieren (CKE5 oder TinyMCE) in Feldern, + * deren bisheriger Wert klassisches HTML statt Content-Builder-JSON enthielt. + */ + initLegacyEditors: function() { + $('.yform-content-builder .content-builder-legacy').each(function() { + var $wrap = $(this); + if ($wrap.data('legacyInitialized')) { + return; + } + $wrap.data('legacyInitialized', true); + + var editor = ($wrap.data('legacyEditor') || '').toString(); + var $textarea = $wrap.find('textarea.content-builder-legacy-textarea').first(); + if ($textarea.length === 0) { + return; + } + + if (editor === 'cke5' && typeof cke5_init === 'function') { + try { cke5_init($textarea); } catch (e) { /* noop */ } + } else if (editor === 'tinymce' && typeof tiny_init === 'function') { + try { tiny_init($wrap); } catch (e) { /* noop */ } + } + }); }, initElementMenuTooltips: function() { @@ -72,6 +99,14 @@ bindEvents: function() { var self = this; + // Legacy-HTML → Content Builder migrieren + $(document).on('click', '.content-builder-legacy-migrate', function(e) { + e.preventDefault(); + e.stopPropagation(); + self.migrateLegacyHtml($(this).closest('.yform-content-builder')); + return false; + }); + // Slice löschen - MUSS VOR edit kommen! $(document).on('click', '.btn-slice-delete', function(e) { e.preventDefault(); @@ -1376,6 +1411,94 @@ }, 500); }, + /** + * Konvertiert einen klassischen HTML-Wert in einen einzelnen + * starter_text-Slice und initialisiert den Content Builder. + */ + migrateLegacyHtml: function($container) { + if ($container.length === 0) { + return; + } + + var $legacy = $container.find('.content-builder-legacy').first(); + if ($legacy.length === 0) { + return; + } + + var $textarea = $legacy.find('textarea.content-builder-legacy-textarea').first(); + if ($textarea.length === 0) { + return; + } + + // Aktuellen HTML-Inhalt holen (TinyMCE / CKE5 in Textarea zurueckschreiben). + var html = ''; + var textareaId = $textarea.attr('id'); + + if (typeof tinymce !== 'undefined' && textareaId) { + var tinyEd = tinymce.get(textareaId); + if (tinyEd) { + try { + tinyEd.save(); + html = tinyEd.getContent(); + } catch (e) { /* noop */ } + } + } + + if (!html && typeof CKEDITOR !== 'undefined' && CKEDITOR.instances && textareaId && CKEDITOR.instances[textareaId]) { + try { html = CKEDITOR.instances[textareaId].getData(); } catch (e) { /* noop */ } + } + + if (!html) { + html = $textarea.val() || ''; + } + + var $confirmBtn = $legacy.find('.content-builder-legacy-migrate'); + var confirmMsg = $confirmBtn.data('confirm'); + if (confirmMsg && !window.confirm(confirmMsg)) { + return; + } + + // Editor-Instanzen sauber entfernen. + if (typeof tinymce !== 'undefined' && textareaId) { + var ed = tinymce.get(textareaId); + if (ed) { try { ed.remove(); } catch (e) { /* noop */ } } + } + if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances && textareaId && CKEDITOR.instances[textareaId]) { + try { CKEDITOR.instances[textareaId].destroy(true); } catch (e) { /* noop */ } + } + + // Form-Input-Name vom Legacy-Textarea uebernehmen, damit der + // submit ohne weitere Aktion den Content-Builder-JSON sendet. + var fieldName = $textarea.attr('name') || ''; + + // Legacy-Block entfernen. + $legacy.remove(); + + // Versteckten Content-Builder-Datenspeicher (neu) anlegen. + var $hidden = $container.find('input.content-builder-data'); + if ($hidden.length === 0 && fieldName) { + $hidden = $('').attr('name', fieldName); + $container.append($hidden); + } + + // Container fuer Slices/Add-Button wieder anzeigen. + $container.find('.content-builder-slices, .content-builder-add').show(); + $container.removeClass('has-legacy-html'); + + // Einen starter_text-Slice mit dem HTML als "text"-Feld anlegen. + var $slicesContainer = $container.find('.content-builder-slices').first(); + this.addSlice($slicesContainer, 'starter_text', 'Text'); + + var $newSlice = $slicesContainer.children('.content-builder-slice').last(); + if ($newSlice.length) { + var sliceData = { text: html }; + $newSlice.attr('data-slice-data', JSON.stringify(sliceData)); + } + + // Hidden-Feld mit dem neuen Slice befuellen. + this.updateHiddenField(); + }, + updateHiddenField: function() { $('.yform-content-builder').each(function() { var $container = $(this); diff --git a/lang/de_de.lang b/lang/de_de.lang index 35b587e..27cfa42 100644 --- a/lang/de_de.lang +++ b/lang/de_de.lang @@ -26,6 +26,15 @@ yform_content_builder_enable_online_toggle = Online/Offline pro Abschnitt yform_content_builder_enable_online_toggle_label = Online/Offline-Schalter für einzelne Abschnitte aktivieren yform_content_builder_enable_online_toggle_notice = Wenn aktiv, erscheint in der Slice-Toolbar ein Augen-Button, mit dem einzelne Abschnitte offline geschaltet werden können. Offline-Abschnitte bleiben in den Daten erhalten, werden aber im Frontend (via yform_content_builder_helper) automatisch ausgeblendet. Gilt nur für die YForm-Variante – die Modulvariante nutzt die normalen REDAXO-Slice-Funktionen. +yform_content_builder_legacy_editor = Legacy-HTML-Editor +yform_content_builder_legacy_editor_none = Deaktiviert +yform_content_builder_legacy_editor_notice = Editor, der angezeigt wird, wenn ein bestehender Wert klassisches HTML statt Content-Builder-JSON enthält. Erlaubt die Migration alter Inhalte in den Content Builder. +yform_content_builder_legacy_profile = Legacy-Editor-Profil +yform_content_builder_legacy_profile_notice = Profilname (Attribute) für CKEditor 5 oder TinyMCE, das für den Legacy-Editor verwendet wird. +yform_content_builder_legacy_notice = Der bestehende Inhalt liegt im klassischen HTML-Format vor. Auf den Content Builder umstellen? +yform_content_builder_legacy_migrate = Jetzt in Content Builder umwandeln +yform_content_builder_legacy_migrate_confirm = Den vorhandenen HTML-Inhalt jetzt als Text-Element in den Content Builder übernehmen? + yform_content_builder_element_add = Element hinzufügen yform_content_builder_element_edit = Bearbeiten yform_content_builder_element_delete = Löschen diff --git a/lang/en_gb.lang b/lang/en_gb.lang index 67a01e9..8b79a7c 100644 --- a/lang/en_gb.lang +++ b/lang/en_gb.lang @@ -47,6 +47,15 @@ yform_content_builder_enable_online_toggle = Online/Offline per section yform_content_builder_enable_online_toggle_label = Enable online/offline switch for individual sections yform_content_builder_enable_online_toggle_notice = When enabled, an eye button appears in the slice toolbar that allows individual sections to be set offline. Offline sections are preserved in the data but automatically hidden in the frontend (via yform_content_builder_helper). Applies only to the YForm variant – the module variant uses the standard REDAXO slice functions. +yform_content_builder_legacy_editor = Legacy HTML editor +yform_content_builder_legacy_editor_none = Disabled +yform_content_builder_legacy_editor_notice = Editor shown when an existing value contains classic HTML instead of Content Builder JSON. Allows migrating legacy content into the Content Builder. +yform_content_builder_legacy_profile = Legacy editor profile +yform_content_builder_legacy_profile_notice = Profile name (attribute) for CKEditor 5 or TinyMCE used for the legacy editor. +yform_content_builder_legacy_notice = The existing value is classic HTML. Switch to the Content Builder? +yform_content_builder_legacy_migrate = Convert to Content Builder +yform_content_builder_legacy_migrate_confirm = Move the existing HTML content into the Content Builder as a text element now? + yform_content_builder_empty = No elements yet. Add an element below. yform_content_builder_delete_confirm = Really delete element? diff --git a/lib/rex_yform_value_content_builder.php b/lib/rex_yform_value_content_builder.php index ad81f98..05d6cec 100644 --- a/lib/rex_yform_value_content_builder.php +++ b/lib/rex_yform_value_content_builder.php @@ -980,7 +980,37 @@ protected function getTemplateVars(): array { $value = $this->parseValue(); $framework = $this->getElement('framework', 'bootstrap'); - + + // Legacy-HTML-Erkennung: + // Wenn der gespeicherte Wert nicht leer ist aber keinen Slice-Array ergibt, + // gehen wir davon aus, dass es sich um klassisches HTML handelt und + // bieten dem Admin die Migration in den Content Builder an. + $rawValue = (string) $this->getValue(); + $legacyHtml = ''; + if ($rawValue !== '' && empty($value)) { + $decoded = json_decode($rawValue, true); + if (!is_array($decoded)) { + $legacyHtml = $rawValue; + } + } + + $addon = rex_addon::get('yform_content_builder'); + $legacyEditor = (string) $this->getElement('legacy_editor', ''); + if ($legacyEditor === '') { + $legacyEditor = (string) $addon->getConfig('legacy_editor', 'none'); + } + if (!in_array($legacyEditor, ['none', 'cke5', 'tinymce'], true)) { + $legacyEditor = 'none'; + } + + $legacyProfile = (string) $this->getElement('legacy_profile', ''); + if ($legacyProfile === '') { + $legacyProfile = (string) $addon->getConfig('legacy_profile', 'default'); + } + if ($legacyProfile === '') { + $legacyProfile = 'default'; + } + return [ 'value' => $value, 'field_type' => 'content_builder', @@ -993,6 +1023,9 @@ protected function getTemplateVars(): array 'description' => $this->getElement('description', ''), 'framework' => $framework, 'available_elements' => $this->getAvailableElements(), + 'legacy_html' => $legacyHtml, + 'legacy_editor' => $legacyEditor, + 'legacy_profile' => $legacyProfile, ]; } @@ -1281,6 +1314,21 @@ public function getDefinitions(): array 'expanded' => false, 'default' => '', ], + 'legacy_editor' => [ + 'type' => 'choice', + 'label' => 'Legacy-HTML-Editor (leer = Addon-Einstellung)', + 'choices' => [ + '' => '-- Addon-Einstellung --', + 'none' => 'Deaktiviert', + 'cke5' => 'CKEditor 5', + 'tinymce' => 'TinyMCE', + ], + 'default' => '', + ], + 'legacy_profile' => [ + 'type' => 'text', + 'label' => 'Legacy-Editor-Profil (leer = Addon-Einstellung)', + ], 'description' => ['type' => 'text', 'label' => 'Beschreibung'], 'notice' => ['type' => 'text', 'label' => rex_i18n::msg('yform_values_defaults_notice')], ], diff --git a/pages/settings.php b/pages/settings.php index 6d5cc27..129642b 100644 --- a/pages/settings.php +++ b/pages/settings.php @@ -11,6 +11,15 @@ $addon->setConfig('theme', rex_post('theme', 'string', '')); $addon->setConfig('compact_mode', rex_post('compact_mode', 'bool', false)); $addon->setConfig('enable_online_toggle', rex_post('enable_online_toggle', 'bool', false)); + + $allowedLegacyEditors = ['none', 'cke5', 'tinymce']; + $legacyEditor = rex_post('legacy_editor', 'string', 'none'); + if (!in_array($legacyEditor, $allowedLegacyEditors, true)) { + $legacyEditor = 'none'; + } + $addon->setConfig('legacy_editor', $legacyEditor); + $addon->setConfig('legacy_profile', rex_post('legacy_profile', 'string', 'default')); + echo rex_view::success(rex_i18n::msg('yform_content_builder_settings_saved')); // Theme Builder Cache zurücksetzen @@ -30,6 +39,11 @@ $currentTheme = $addon->getConfig('theme', ''); $compactMode = $addon->getConfig('compact_mode', false); $enableOnlineToggle = $addon->getConfig('enable_online_toggle', false); +$legacyEditor = $addon->getConfig('legacy_editor', 'none'); +if (!in_array($legacyEditor, ['none', 'cke5', 'tinymce'], true)) { + $legacyEditor = 'none'; +} +$legacyProfile = (string) $addon->getConfig('legacy_profile', 'default'); // Formular bauen $content = ''; @@ -64,6 +78,29 @@ $n['note'] = rex_i18n::msg('yform_content_builder_enable_online_toggle_notice'); $formElements[] = $n; +// Legacy-Editor (HTML-Erkennung) +$legacyEditorChoices = [ + 'none' => rex_i18n::msg('yform_content_builder_legacy_editor_none'), + 'cke5' => 'CKEditor 5', + 'tinymce' => 'TinyMCE', +]; +$n = []; +$n['label'] = ''; +$n['field'] = ''; +$n['note'] = rex_i18n::msg('yform_content_builder_legacy_editor_notice'); +$formElements[] = $n; + +$n = []; +$n['label'] = ''; +$n['field'] = ''; +$n['note'] = rex_i18n::msg('yform_content_builder_legacy_profile_notice'); +$formElements[] = $n; + $fragment = new rex_fragment(); $fragment->setVar('elements', $formElements, false); $content .= $fragment->parse('core/form/form.php'); diff --git a/ytemplates/bootstrap/value.content_builder.tpl.php b/ytemplates/bootstrap/value.content_builder.tpl.php index 9a4f713..67dfcc1 100644 --- a/ytemplates/bootstrap/value.content_builder.tpl.php +++ b/ytemplates/bootstrap/value.content_builder.tpl.php @@ -10,6 +10,9 @@ * @var string $description * @var string $framework * @var array $available_elements + * @var string $legacy_html + * @var string $legacy_editor + * @var string $legacy_profile */ $fieldClass = 'yform-content-builder'; @@ -17,6 +20,14 @@ $fieldClass .= ' required'; } +$legacyHtml = isset($legacy_html) ? (string) $legacy_html : ''; +$legacyEditor = isset($legacy_editor) ? (string) $legacy_editor : 'none'; +$legacyProfile = isset($legacy_profile) ? (string) $legacy_profile : 'default'; +$hasLegacyHtml = ($legacyHtml !== '' && $legacyEditor !== 'none'); +if ($hasLegacyHtml) { + $fieldClass .= ' has-legacy-html'; +} + // Kompaktmodus aus Addon-Config laden $addon = rex_addon::get('yform_content_builder'); if ($addon->getConfig('compact_mode')) { @@ -47,6 +58,8 @@
= $description ?>
- -= $notice ?>
From 916b3d9b20d3d6a4914413e04102f13aad098c8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 17:31:13 +0000 Subject: [PATCH 3/3] Use ckeditors global for CKE5 in legacy migration and add field-id to legacy editor id --- assets/content-builder.js | 8 ++++---- ytemplates/bootstrap/value.content_builder.tpl.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/content-builder.js b/assets/content-builder.js index ba6d36d..ebe5604 100644 --- a/assets/content-builder.js +++ b/assets/content-builder.js @@ -1444,8 +1444,8 @@ } } - if (!html && typeof CKEDITOR !== 'undefined' && CKEDITOR.instances && textareaId && CKEDITOR.instances[textareaId]) { - try { html = CKEDITOR.instances[textareaId].getData(); } catch (e) { /* noop */ } + if (!html && typeof ckeditors !== 'undefined' && textareaId && ckeditors[textareaId]) { + try { html = ckeditors[textareaId].getData(); } catch (e) { /* noop */ } } if (!html) { @@ -1463,8 +1463,8 @@ var ed = tinymce.get(textareaId); if (ed) { try { ed.remove(); } catch (e) { /* noop */ } } } - if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances && textareaId && CKEDITOR.instances[textareaId]) { - try { CKEDITOR.instances[textareaId].destroy(true); } catch (e) { /* noop */ } + if (typeof ckeditors !== 'undefined' && textareaId && ckeditors[textareaId]) { + try { ckeditors[textareaId].destroy(); } catch (e) { /* noop */ } } // Form-Input-Name vom Legacy-Textarea uebernehmen, damit der diff --git a/ytemplates/bootstrap/value.content_builder.tpl.php b/ytemplates/bootstrap/value.content_builder.tpl.php index 67dfcc1..e79375a 100644 --- a/ytemplates/bootstrap/value.content_builder.tpl.php +++ b/ytemplates/bootstrap/value.content_builder.tpl.php @@ -73,7 +73,7 @@ getId() . '_' . uniqid(); $legacyInputName = 'FORM[' . $this->params['form_name'] . '][' . $this->getId() . ']'; ?>