From a441115abb352fb53bfbfed5e9786e371cabb274 Mon Sep 17 00:00:00 2001
From: Michael Danielczok <michael@pcsg.de>
Date: Mon, 17 Mar 2025 14:10:18 +0100
Subject: [PATCH] feat: new sitetype: external content

Related: quiqqer/package-sitetypes#19
---
 bin/Controls/backend/ExternalContent.html     |  71 ++++++++
 bin/Controls/backend/ExternalContent.js       | 167 ++++++++++++++++++
 locale.xml                                    |  79 +++++----
 site.xml                                      |  51 ++----
 src/QUI/Controls/ExternalContent.css          |  25 +++
 src/QUI/Controls/ExternalContent.html         |  21 +++
 .../{InlineFrame.php => ExternalContent.php}  |  41 +++--
 src/QUI/Controls/InlineFrame.css              |  14 --
 src/QUI/Controls/InlineFrame.html             |  15 --
 types/{iframe.html => externalContent.html}   |   2 +-
 types/externalContent.php                     |  41 +++++
 types/iframe.php                              |  20 ---
 12 files changed, 411 insertions(+), 136 deletions(-)
 create mode 100644 bin/Controls/backend/ExternalContent.html
 create mode 100644 bin/Controls/backend/ExternalContent.js
 create mode 100644 src/QUI/Controls/ExternalContent.css
 create mode 100644 src/QUI/Controls/ExternalContent.html
 rename src/QUI/Controls/{InlineFrame.php => ExternalContent.php} (56%)
 delete mode 100644 src/QUI/Controls/InlineFrame.css
 delete mode 100644 src/QUI/Controls/InlineFrame.html
 rename types/{iframe.html => externalContent.html} (89%)
 create mode 100644 types/externalContent.php
 delete mode 100644 types/iframe.php

diff --git a/bin/Controls/backend/ExternalContent.html b/bin/Controls/backend/ExternalContent.html
new file mode 100644
index 0000000..1977532
--- /dev/null
+++ b/bin/Controls/backend/ExternalContent.html
@@ -0,0 +1,71 @@
+<table class="data-table data-table-flexbox" style="margin-bottom: 0; border: none;">
+    <tbody>
+    <tr>
+        <td style="padding-inline: 0;">
+            <label class="field-container">
+                <span class="field-container-item">
+                    {{typeTitle}}
+                </span>
+                <select name="externalContent-type" style="width: 100%;" data-ref="select">
+                    <option value="text">{{typeOptionText}}</option>
+                    <option value="iframe">{{typeOptionIFrame}}</option>
+                </select>
+            </label>
+        </td>
+    </tr>
+
+    <tr data-ref="variable-option" data-type="text">
+        <td style="padding-inline: 0;">
+            <label class="field-container">
+                <span class="field-container-item">
+                    {{textTitle}}
+                </span>
+                <textarea name="externalContent-text" class="field-container-field field-description"
+                          rows="12"></textarea>
+            </label>
+        </td>
+    </tr>
+
+    <tr data-ref="variable-option" data-type="iframe">
+        <td style="padding-inline: 0;">
+            <label class="field-container">
+                <span class="field-container-item">
+                    {{urlTitle}}
+                </span>
+                <input name="externalContent-url" class="field-container-field field-title"/>
+            </label>
+        </td>
+    </tr>
+    <tr data-ref="variable-option" data-type="iframe">
+        <td style="padding-inline: 0;">
+            <label class="field-container">
+                <span class="field-container-item">
+                    {{heightDesktopTitle}}
+                </span>
+                <input name="externalContent-height_desktop" class="field-container-field field-text"/>
+            </label>
+        </td>
+    </tr>
+    <tr data-ref="variable-option" data-type="iframe">
+        <td style="padding-top: 0; padding-inline: 0">
+            <div class="description"> {{{heightDesktopDesc}}}</div>
+        </td>
+    </tr>
+    <tr data-ref="variable-option" data-type="iframe">
+        <td style="padding-inline: 0;">
+            <label class="field-container">
+                <div class="field-container-item">
+                    {{heightMobileTitle}}
+
+                </div>
+                <input name="externalContent-height_mobile" class="field-container-field field-text"/>
+            </label>
+        </td>
+    </tr>
+    <tr data-ref="variable-option" data-type="iframe">
+        <td style="padding-top: 0; padding-inline: 0">
+            <div class="description"> {{{heightMobileDesc}}}</div>
+        </td>
+    </tr>
+    </tbody>
+</table>
diff --git a/bin/Controls/backend/ExternalContent.js b/bin/Controls/backend/ExternalContent.js
new file mode 100644
index 0000000..446e6aa
--- /dev/null
+++ b/bin/Controls/backend/ExternalContent.js
@@ -0,0 +1,167 @@
+/**
+ *
+ * @module package/quiqqer/sitetypes/bin/Controls/backend/ExternalContent
+ * @author Michael Danielczok
+ *
+ */
+
+define('package/quiqqer/sitetypes/bin/Controls/backend/ExternalContent', [
+
+    'qui/controls/Control',
+    'Locale',
+    'Mustache',
+
+    'text!package/quiqqer/sitetypes/bin/Controls/backend/ExternalContent.html',
+
+], function(
+    QUIControl,
+    QUILocale,
+    Mustache,
+    template
+) {
+    'use strict';
+
+    const lg = 'quiqqer/sitetypes';
+
+    return new Class({
+
+        Extends: QUIControl,
+        Type: 'package/quiqqer/sitetypes/bin/Controls/backend/ExternalContent',
+
+        Binds: [
+            '$onImport'
+        ],
+
+        initialize: function(options) {
+            this.parent(options);
+
+            this.Select = null;
+            this.variableOptionsNode = null; // NodeList of <tr> elements
+            this.data = {};
+
+            this.addEvents({
+                onImport: this.$onImport
+            });
+        },
+
+        /**
+         * event: on import
+         */
+        $onImport: function() {
+            this.$Input = this.getElm();
+
+            this.$loadDataFromInput();
+            this.$renderLayout();
+
+            this.Select.addEventListener('change', (e) => {
+                this.selectedType = e.target.value;
+                this.toggleVariableOption();
+            });
+
+            this.$setValuesToInputs();
+
+            this.variableOptionsNode = this.$Elm.querySelectorAll('[data-ref="variable-option"]');
+
+            this.toggleVariableOption();
+        },
+
+        /**
+         * Loads data from the hidden input field.
+         */
+        $loadDataFromInput: function () {
+            const jsonData = this.$Input.get('value');
+
+            if (jsonData) {
+                try {
+                    this.data = JSON.parse(jsonData);
+                } catch (e) {
+                    this.data = {};
+                    console.warn('Invalid JSON in ExternalContent input:', e);
+                }
+            } else {
+                this.data = {};
+            }
+        },
+
+        /**
+         * Generates the HTML structure of the component.
+         */
+        $renderLayout: function () {
+            this.$Elm = new Element('div', {
+                'class': 'quiqqer-sitetypes-backend-settings-externalContent',
+                styles: {
+                    width: '100%'
+                }
+            }).wraps(this.$Input);
+
+            const prefix = 'sitetypes.externalContent.settings.';
+            const texts = {
+                typeTitle: QUILocale.get(lg, prefix + 'type.title'),
+                typeOptionText: QUILocale.get(lg, prefix + 'type.option.text'),
+                typeOptionIFrame: QUILocale.get(lg, prefix + 'type.option.iframe'),
+                textTitle: QUILocale.get(lg, prefix + 'text.title'),
+                urlTitle: QUILocale.get(lg, prefix + 'url.title'),
+                heightDesktopTitle: QUILocale.get(lg, prefix + 'heightDesktop.title'),
+                heightDesktopDesc: QUILocale.get(lg, prefix + 'heightDesktop.desc'),
+                heightMobileTitle: QUILocale.get(lg, prefix + 'heightMobile.title'),
+                heightMobileDesc: QUILocale.get(lg, prefix + 'heightMobile.desc'),
+            };
+
+            new Element('div', {
+                html: Mustache.render(template, texts)
+            }).inject(this.$Elm);
+
+            this.Select = this.$Elm.querySelector('[data-ref="select"]');
+            this.selectedType = 'text';
+        },
+
+        /**
+         * Sets the values of the input fields, selects and textareas based on the current data.
+         */
+        $setValuesToInputs: function () {
+            const inputs = this.$Elm.getElements('input, select, textarea');
+
+            inputs.each((input) => {
+                input.addEventListener('blur', this.onInputBlur.bind(this));
+
+                const name = input.getAttribute('name');
+                const value = this.data[name];
+
+                if (!value) return;
+
+                if (input.type === 'select-one') {
+                    input.value = value;
+                    this.selectedType = value;
+                } else {
+                    input.set('value', value);
+                }
+            });
+        },
+
+        /**
+         * Toggles the visibility of variable options based on the selected type.
+         */
+        toggleVariableOption: function() {
+            this.variableOptionsNode.forEach((Node) => {
+                if (Node.getAttribute('data-type') === this.selectedType) {
+                    Node.style.display = null;
+                } else {
+                    Node.style.display = 'none';
+                }
+            });
+        },
+
+        /**
+         * Handles the blur event of input fields, selects and textareas.
+         * Updates the data object with the new value and sets the hidden input field.
+         *
+         * @param {Event} event - The blur event object.
+         */
+        onInputBlur: function(event) {
+            const input = event.target;
+            this.data[input.getAttribute('name')] = input.get('value');
+            const jsonData = JSON.stringify(this.data);
+            this.$Input.set('value', jsonData);
+        }
+    });
+});
diff --git a/locale.xml b/locale.xml
index f69e257..cb75ff6 100644
--- a/locale.xml
+++ b/locale.xml
@@ -24,13 +24,13 @@ oder verlinken beispielsweise im Footer auf die Seite. In den Einstellungen des
 Haben Sie z.B. Ihre Community auf einer externen Webseite, leiten Sie den Nutzer von Ihrer Weiterleitungs-Seite "Community" auf die externe Seite um.]]></de>
             <en><![CDATA[]]></en>
         </locale>
-        <locale name="admin.types.iframe">
-            <de><![CDATA[iFrame]]></de>
-            <en><![CDATA[iFrame]]></en>
+        <locale name="admin.types.externalContent">
+            <de><![CDATA[Externer Inhalt]]></de>
+            <en><![CDATA[External content]]></en>
         </locale>
-        <locale name="admin.types.iframe.description">
-            <de><![CDATA[Ein iFrame-Seitentyp ermöglicht es, eine externe Webseite oder ein anderes HTML-Dokument innerhalb einer eigenen Webseite zu laden und anzuzeigen.]]></de>
-            <en><![CDATA[A page type of iFrame allows you to load and display an external website or another HTML document within your own website.]]></en>
+        <locale name="admin.types.externalContent.description">
+            <de><![CDATA[Der Seitentyp ermöglicht es, eine externe Webseite oder ein anderes HTML-Dokument innerhalb einer eigenen Webseite zu laden und anzuzeigen.]]></de>
+            <en><![CDATA[This site type allows you to load and display an external website or another HTML document within your own website.]]></en>
         </locale>
         <locale name="quiqqer/sitetypes:types/list.title">
             <de><![CDATA[Standard-Liste]]></de>
@@ -117,6 +117,47 @@ Haben Sie z.B. Ihre Community auf einer externen Webseite, leiten Sie den Nutzer
             The page type automatically serves the QUIQQER system to access the content of this type and display it in a suitable place.
             ]]></en>
         </locale>
+
+        <locale name="sitetypes.externalContent.settings.title">
+            <de><![CDATA[Externer Inhalt]]></de>
+            <en><![CDATA[External content]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.type.title">
+            <de><![CDATA[Externer Inhalt Typ]]></de>
+            <en><![CDATA[External content type]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.type.option.text">
+            <de><![CDATA[Text Eingabe]]></de>
+            <en><![CDATA[Text input]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.type.option.iframe">
+            <de><![CDATA[iFrame]]></de>
+            <en><![CDATA[iFrame]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.text.title">
+            <de><![CDATA[Externer Inhalt]]></de>
+            <en><![CDATA[External content]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.url.title">
+            <de><![CDATA[Link]]></de>
+            <en><![CDATA[Url]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.heightDesktop.title">
+            <de><![CDATA[Höhe des iFrames]]></de>
+            <en><![CDATA[Height of iFrame]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.heightDesktop.desc">
+            <de><![CDATA[Erlaubt sind alle valide CSS Werte, z.B. 250px, 100%, 50vh, etc. Wert ohne Einheit wird als Pixel interpretiert.]]></de>
+            <en><![CDATA[Allowed are all valid CSS values, e.g. 250px, 100%, 50vh, etc. Values without a unit are interpreted as pixels.]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.heightMobile.title">
+            <de><![CDATA[Höhe des iFrames (auf mobilen Geräten)]]></de>
+            <en><![CDATA[Height of iFrame (on mobile devices)]]></en>
+        </locale>
+        <locale name="sitetypes.externalContent.settings.heightMobile.desc">
+            <de><![CDATA[Erlaubt sind alle valide CSS Werte, z.B. 250px, 100%, 50vh, etc. Wert ohne Einheit wird als Pixel interpretiert.<br>Diese Einstellung ist optional. Wird kein Wert eingetragen, wird die Desktop-Einstellung verwendet.]]></de>
+            <en><![CDATA[Allowed are all valid CSS values, e.g. 250px, 100%, 50vh, etc. Values without a unit are interpreted as pixels.<br>This setting is optional. If no value is entered, the desktop setting is used.]]></en>
+        </locale>
     </groups>
     <groups name="quiqqer/sitetypes" datatype="php">
         <locale name="quiqqer.sitetypes.privacypolicy.settings.title">
@@ -382,32 +423,6 @@ Haben Sie z.B. Ihre Community auf einer externen Webseite, leiten Sie den Nutzer
             <en><![CDATA[Target page]]></en>
         </locale>
 
-
-        <locale name="sitetypes.iframe.settings.title">
-            <de><![CDATA[iFrame]]></de>
-            <en><![CDATA[iFrame]]></en>
-        </locale>
-        <locale name="sitetypes.iframe.settings.url">
-            <de><![CDATA[Link]]></de>
-            <en><![CDATA[Url]]></en>
-        </locale>
-        <locale name="sitetypes.iframe.settings.height.desktop">
-            <de><![CDATA[Höhe des iFrames]]></de>
-            <en><![CDATA[Height of iFrame]]></en>
-        </locale>
-        <locale name="sitetypes.iframe.settings.height.desktop.desc">
-            <de><![CDATA[Erlaubt sind alle valide CSS Werte, z.B. 250px, 100%, 50vh, etc. Wert ohne Einheit wird als Pixel interpretiert.]]></de>
-            <en><![CDATA[Allowed are all valid CSS values, e.g. 250px, 100%, 50vh, etc. Values without a unit are interpreted as pixels.]]></en>
-        </locale>
-        <locale name="sitetypes.iframe.settings.height.mobile">
-            <de><![CDATA[Höhe des iFrames (auf mobilen Geräten)]]></de>
-            <en><![CDATA[Height of iFrame (on mobile devices)]]></en>
-        </locale>
-        <locale name="sitetypes.iframe.settings.height.mobile.desc">
-            <de><![CDATA[Erlaubt sind alle valide CSS Werte, z.B. 250px, 100%, 50vh, etc. Wert ohne Einheit wird als Pixel interpretiert.<br>Diese Einstellung ist optional. Wird kein Wert eingetragen, wird die Desktop-Einstellung verwendet.]]></de>
-            <en><![CDATA[Allowed are all valid CSS values, e.g. 250px, 100%, 50vh, etc. Values without a unit are interpreted as pixels.<br>This setting is optional. If no value is entered, the desktop setting is used.]]></en>
-        </locale>
-
         <locale name="tpl.search.no.results">
             <de><![CDATA[Keine Ergebnisse gefunden. Bitte nutzen Sie einen anderen Suchbegriff.]]></de>
             <en><![CDATA[No results found. Please use a different search.]]></en>
diff --git a/site.xml b/site.xml
index f333a5d..493d0eb 100644
--- a/site.xml
+++ b/site.xml
@@ -260,59 +260,32 @@
             </settings>
         </type>
 
-        <!-- iframe -->
-        <type type="types/iframe" icon="fa-solid fa-laptop-code">
-            <locale group="quiqqer/sitetypes" var="admin.types.iframe"/>
+        <!-- externalContent (ifrmae) -->
+        <type type="types/externalContent" icon="fa-solid fa-laptop-code">
+            <locale group="quiqqer/sitetypes" var="admin.types.externalContent"/>
             <desc>
-                <locale group="quiqqer/sitetypes" var="admin.types.iframe.desc"/>
+                <locale group="quiqqer/sitetypes" var="admin.types.externalContent.desc"/>
             </desc>
 
             <attributes>
-                <attribute>quiqqer.settings.sitetypes.iframe.url</attribute>
-                <attribute>quiqqer.settings.sitetypes.iframe.height.desktop</attribute>
-                <attribute>quiqqer.settings.sitetypes.iframe.height.mobile</attribute>
+                <attribute>quiqqer.settings.sitetypes.externalContent.settings</attribute>
             </attributes>
 
             <settings>
-                <category name="sitetypes-iframe-settings">
+                <category name="sitetypes-externalContent-settings">
                     <settings>
                         <title>
                             <locale group="quiqqer/sitetypes"
-                                    var="sitetypes.iframe.settings.title"
+                                    var="sitetypes.externalContent.settings.title"
                             />
                         </title>
 
-                        <input conf="quiqqer.settings.sitetypes.iframe.url">
-                            <text>
-                                <locale group="quiqqer/sitetypes"
-                                        var="sitetypes.iframe.settings.url"
-                                />
-                            </text>
-                        </input>
-                        <input conf="quiqqer.settings.sitetypes.iframe.height.desktop">
-                            <text>
-                                <locale group="quiqqer/sitetypes"
-                                        var="sitetypes.iframe.settings.height.desktop"
-                                />
-                            </text>
-                            <description>
-                                <locale group="quiqqer/sitetypes"
-                                        var="sitetypes.iframe.settings.height.desktop.desc"
-                                />
-                            </description>
-                        </input>
-                        <input conf="quiqqer.settings.sitetypes.iframe.height.mobile">
-                            <text>
-                                <locale group="quiqqer/sitetypes"
-                                        var="sitetypes.iframe.settings.height.mobile"
-                                />
-                            </text>
-                            <description>
-                                <locale group="quiqqer/sitetypes"
-                                        var="sitetypes.iframe.settings.height.mobile.desc"
-                                />
-                            </description>
+                        <input conf="quiqqer.settings.sitetypes.externalContent.settings" type="hidden"
+                                 data-qui="package/quiqqer/sitetypes/bin/Controls/backend/ExternalContent"
+                        >
+
                         </input>
+
                     </settings>
                 </category>
             </settings>
diff --git a/src/QUI/Controls/ExternalContent.css b/src/QUI/Controls/ExternalContent.css
new file mode 100644
index 0000000..8a2fed7
--- /dev/null
+++ b/src/QUI/Controls/ExternalContent.css
@@ -0,0 +1,25 @@
+.quiqqer-sitetypes-controls-externalContent {
+    --_qui-sitetypes-controls-externalContent-contentText-height: var(--qui-sitetypes-controls-externalContent-contentText-height, auto);
+}
+
+/* text */
+.quiqqer-sitetypes-controls-externalContent__contentText {
+    height: var(--_qui-sitetypes-controls-externalContent-contentText-height);
+}
+
+/* iframe */
+.quiqqer-sitetypes-externalContent__iframe {
+    display: block;
+    border: none;
+    height: var(--_qui-sitetypes-controls-externalContent-iframe-height--desktop);
+    width: var(--_qui-sitetypes-controls-externalContent-iframe-width);
+    max-width: 100%;
+    margin-inline: auto;
+}
+
+@media screen and (max-width: 767px) {
+
+    .quiqqer-sitetypes-externalContent__iframe {
+        height: var(--_qui-sitetypes-controls-externalContent-iframe-height--mobile);
+    }
+}
\ No newline at end of file
diff --git a/src/QUI/Controls/ExternalContent.html b/src/QUI/Controls/ExternalContent.html
new file mode 100644
index 0000000..a32c565
--- /dev/null
+++ b/src/QUI/Controls/ExternalContent.html
@@ -0,0 +1,21 @@
+{if $this->getAttribute('showTitle') && $this->getAttribute('frontendTitle')}
+    <header class="control-header">
+        <h1>{$this->getAttribute('frontendTitle')}</h1>
+    </header>
+{/if}
+
+{if $this->getAttribute('content') != ""}
+    <div class="control-content">
+        {$this->getAttribute('content')}
+    </div>
+{/if}
+
+<div class="control-template quiqqer-sitetypes-controls-externalContent-controlWrapper">
+    {if $type === 'text' && $externalContentText}
+        <div class="quiqqer-sitetypes-controls-externalContent__contentText">
+            {$externalContentText}
+        </div>
+    {elseif $iframeUrl}
+        <iframe src="{$iframeUrl}" class="quiqqer-sitetypes-externalContent__iframe"></iframe>
+    {/if}
+</div>
\ No newline at end of file
diff --git a/src/QUI/Controls/InlineFrame.php b/src/QUI/Controls/ExternalContent.php
similarity index 56%
rename from src/QUI/Controls/InlineFrame.php
rename to src/QUI/Controls/ExternalContent.php
index 8b62a30..f75d7b4 100644
--- a/src/QUI/Controls/InlineFrame.php
+++ b/src/QUI/Controls/ExternalContent.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * This file contains QUI\Controls\InlineFrame
+ * This file contains QUI\Controls\ExternalContent
  */
 
 namespace QUI\Controls;
@@ -17,11 +17,11 @@
 use function is_array;
 
 /**
- * Class InlineFrame
+ * Class ExternalContent
  *
  * @package quiqqer/sitetypes
  */
-class InlineFrame extends QUI\Control
+class ExternalContent extends QUI\Control
 {
     /**
      * constructor
@@ -32,8 +32,10 @@ public function __construct(array $attributes = [])
     {
         // default options
         $this->setAttributes([
-            'class' => 'quiqqer-sitetypes-controls-inlineframe',
-            'url' => '',
+            'class' => 'quiqqer-sitetypes-controls-externalContent',
+            'externalContentType' => 'text', // 'text' or 'iframe'
+            'externalContentText' => '', // it may by script tag and / or HTML Node <div>
+            'iframeUrl' => '',
             'iFrameHeightDesktop' => 400,
             'iFrameHeightMobile' => '',
             'iFrameWidth' => '100%'
@@ -41,7 +43,7 @@ public function __construct(array $attributes = [])
 
         parent::__construct($attributes);
 
-        $this->addCSSFile(dirname(__FILE__) . '/InlineFrame.css');
+        $this->addCSSFile(dirname(__FILE__) . '/ExternalContent.css');
 
         $this->setAttribute('cacheable', 0);
     }
@@ -59,7 +61,13 @@ public function getBody(): string
     {
         $Engine = QUI::getTemplateManager()->getEngine();
 
-        if (!$this->getAttribute('url')) {
+        $type = $this->getAttribute('externalContentType');
+        $externalContentText = $this->getAttribute('externalContentText');
+        $iframeUrl = $this->getAttribute('iframeUrl');
+
+        if (!$externalContentText && !$iframeUrl) {
+            QUI\System\Log::addWarning('QUI\Controls\ExternalContent: nor externalContentText or iframeUrl found.');
+
             return '';
         }
 
@@ -71,13 +79,16 @@ public function getBody(): string
             $heightMobile = $heightDesktop;
         }
 
-        $this->setCustomVariable('height--desktop', $this->getValue($heightDesktop));
-        $this->setCustomVariable('height--mobile', $this->getValue($heightMobile));
-        $this->setCustomVariable('width', $this->getValue($width));
+        $this->setCustomVariable('iframe-height--desktop', $this->getValue($heightDesktop));
+        $this->setCustomVariable('iframe-height--mobile', $this->getValue($heightMobile));
+        $this->setCustomVariable('iframe-width', $this->getValue($width));
+        $this->setCustomVariable('iframe-width', $this->getValue($width));
 
         $Engine->assign([
             'this' => $this,
-            'url' => $this->getAttribute('url')
+            'type' => $type,
+            'externalContentText' => $externalContentText,
+            'iframeUrl' => $iframeUrl
         ]);
 
         return $Engine->fetch($this->getTemplateFile());
@@ -104,10 +115,10 @@ protected function getValue($value): string
 
     /**
      * Set custom css variable to the control as inline style
-     *   --_qui-sitetypes-controls-inlineFrame-$name: var(--qui-sitetypes-controls-inlineFrame-$name, $value);
+     *   --_qui-sitetypes-controls-externalContent-$name: var(--qui-sitetypes-controls-externalContent-$name, $value);
      *
      * Example:
-     *   --_qui-sitetypes-controls-inlineFrame-iFrameHeight--desktop: var(--qui-sitetypes-controls-inlineFrame-iFrameHeight--desktop, '50vh');
+     *   --_qui-sitetypes-controls-externalContent-iFrameHeight--desktop: var(--qui-sitetypes-controls-externalContent-iFrameHeight--desktop, '50vh');
      *
      * @param string $name
      * @param string $value
@@ -121,8 +132,8 @@ private function setCustomVariable(string $name, string $value): void
         }
 
         $this->setStyle(
-            '--_qui-sitetypes-controls-inlineFrame-' . $name,
-            'var(--qui-sitetypes-controls-inlineFrame-' . $name . ', ' . $value . ')'
+            '--_qui-sitetypes-controls-externalContent-' . $name,
+            'var(--qui-sitetypes-controls-externalContent-' . $name . ', ' . $value . ')'
         );
     }
 }
diff --git a/src/QUI/Controls/InlineFrame.css b/src/QUI/Controls/InlineFrame.css
deleted file mode 100644
index 9a76b33..0000000
--- a/src/QUI/Controls/InlineFrame.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.quiqqer-sitetypes-inlineframe__iframe {
-    display: block;
-    border: none;
-    height: var(--_qui-sitetypes-controls-inlineFrame-height--desktop);
-    width: var(--_qui-sitetypes-controls-inlineFrame-width);
-    max-width: 100%;
-    margin-inline: auto;
-}
-
-@media screen and (max-width: 767px) {
-    .quiqqer-sitetypes-inlineframe__iframe {
-        height: var(--_qui-sitetypes-controls-inlineFrame-height--mobile);
-    }
-}
\ No newline at end of file
diff --git a/src/QUI/Controls/InlineFrame.html b/src/QUI/Controls/InlineFrame.html
deleted file mode 100644
index 6f0ef24..0000000
--- a/src/QUI/Controls/InlineFrame.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{if $this->getAttribute('showTitle') && $this->getAttribute('frontendTitle')}
-    <header class="control-header">
-        <h1>{$this->getAttribute('frontendTitle')}</h1>
-    </header>
-{/if}
-
-{if $this->getAttribute('content') != ""}
-    <div class="control-content">
-        {$this->getAttribute('content')}
-    </div>
-{/if}
-
-<div class="control-template quiqqer-sitetypes-controls-inlineframe-controlWrapper">
-    <iframe src="{$url}" class="quiqqer-sitetypes-inlineframe__iframe"></iframe>
-</div>
\ No newline at end of file
diff --git a/types/iframe.html b/types/externalContent.html
similarity index 89%
rename from types/iframe.html
rename to types/externalContent.html
index a34dbc3..95df18f 100644
--- a/types/iframe.html
+++ b/types/externalContent.html
@@ -5,5 +5,5 @@
 {/if}
 
 <section class="content-template">
-    {$InlineFrame->create()}
+    {$ExternalContent->create()}
 </section>
\ No newline at end of file
diff --git a/types/externalContent.php b/types/externalContent.php
new file mode 100644
index 0000000..2b2b57f
--- /dev/null
+++ b/types/externalContent.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * This file contains the iframe site type
+ *
+ * @var QUI\Projects\Project $Project
+ * @var QUI\Projects\Site $Site
+ * @var QUI\Interfaces\Template\EngineInterface $Engine
+ * @var QUI\Template $Template
+ **/
+
+
+$settingsRaw = $Site->getAttribute('quiqqer.settings.sitetypes.externalContent.settings');
+
+if (!$settingsRaw || !is_string($settingsRaw)) {
+    QUI\System\Log::addWarning('ExternalContent: No settings found or settings are not a valid string.');
+
+    return;
+}
+
+$settings = json_decode($settingsRaw, true);
+
+if (json_last_error() !== JSON_ERROR_NONE || !is_array($settings)) {
+    QUI\System\Log::addWarning(
+        'Site type external content: Failed to decode settings – JSON error: ' . json_last_error_msg()
+    );
+
+    return;
+}
+
+$ExternalContent = new QUI\Controls\ExternalContent([
+    'externalContentType' => $settings['externalContent-type'] ?? 'text',
+    'externalContentText' => $settings['externalContent-text'] ?? '',
+    'iframeUrl' => $settings['externalContent-url'] ?? '',
+    'iFrameHeightDesktop' => $settings['externalContent-height_desktop'] ?? null,
+    'iFrameHeightMobile' => $settings['externalContent-height_mobile'] ?? null
+]);
+
+$Engine->assign([
+    'ExternalContent' => $ExternalContent
+]);
diff --git a/types/iframe.php b/types/iframe.php
deleted file mode 100644
index 638fb80..0000000
--- a/types/iframe.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * This file contains the iframe site type
- *
- * @var QUI\Projects\Project $Project
- * @var QUI\Projects\Site $Site
- * @var QUI\Interfaces\Template\EngineInterface $Engine
- * @var QUI\Template $Template
- **/
-
-$InlineFrame = new QUI\Controls\InlineFrame([
-    'url' => $Site->getAttribute('quiqqer.settings.sitetypes.iframe.url'),
-    'iFrameHeightDesktop' => $Site->getAttribute('quiqqer.settings.sitetypes.iframe.height.desktop'),
-    'iFrameHeightMobile' => $Site->getAttribute('quiqqer.settings.sitetypes.iframe.height.mobile'),
-]);
-
-$Engine->assign([
-    'InlineFrame' => $InlineFrame
-]);
-- 
GitLab