diff --git a/CHANGELOG.md b/CHANGELOG.md index e71bc2c2..e9b3091f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,33 @@ Changelog ========= +Version 8.10.3 +--------------- + +### Fixes + +* **for_images:** „Aus Mediapool austauschen“ – Swap-Button delegiert jetzt an `mceImage` und nutzt damit den Standard-`file_picker_callback`. +* **Issue #171:** Deaktivierte `for_*`-Plugins werden nicht mehr initialisiert. `base.js` filtert `external_plugins` jetzt gegen die `plugins:`-Liste des Profils. + +### Profil-Assistent + +* Pluginliste ↔ Optionen sind jetzt bidirektional gekoppelt: `for_images` deaktivieren entfernt automatisch den Bildbreiten-Block; `autoresize`-Checkbox ↔ `autoresize`-Plugin synchron. +* **Auto-Hide Toolbar** neu implementiert: Menü/Toolbar/Statusleiste klappen via CSS `:focus-within`/`:hover` ein/aus statt per fragilem Inline-`display:none`. Status wird beim Reload korrekt erkannt. +* Sub-Bereich „Bild- und FOR Images Einstellungen“ umbenannt (vorher „Bildbreiten“). +* Schritt 1 kompakter gruppiert: Menüleiste + Auto-Hide Toolbar in einer Zeile, URL/Encoding in einer statt zwei Zeilen, einheitliche Bar-Style-Gruppentitel mit Light/Dark-Mode-Support. + +### Build-Pipeline + +* Custom-Plugins werden nur noch nach `assets/scripts/tinymce/plugins/` gebaut; `assets/vendor/tinymce/` ist ab sofort reserviert für upstream TinyMCE. `clean-build.js` räumt Altlasten. Details in `DEVS.md`. + +### Intern (`for_images`) + +* `Plugin.ts` entrümpelt: einheitliche Klassenbereinigung über `cleanupFigureClasses`, einfacher Traversal in `normalizeFigures`, Cut/Copy/Delete-Handler durch zwei Helper konsolidiert (~40 Zeilen weniger, gleiches Verhalten). + +### Docs + +Überarbeitet und besser strukturiert + Version 8.10.2 --------------- diff --git a/DEVS.md b/DEVS.md index 197dbbf9..81c3e81a 100644 --- a/DEVS.md +++ b/DEVS.md @@ -1,68 +1,111 @@ -# Entwickler-Dokumentation (Stand: aktueller AddOn-Code) +# Entwickler-Dokumentation -Diese Seite beschreibt nur den derzeitigen Ist-Stand des TinyMCE-AddOns. +Diese Seite richtet sich an Entwickler, die das TinyMCE-AddOn erweitern oder andere REDAXO-AddOns daran anschließen wollen. Sie beschreibt ausschließlich den aktuellen Aufbau. -## Architektur in Kurzform +## Inhalt -- Profile liegen in der DB-Tabelle `rex_tinymce_profiles`. -- Die aktive Konfigurationsquelle ist ausschließlich die Spalte `profile`. -- Die Legacy-Felder `plugins`, `toolbar` und `extra` werden nicht mehr als eigene DB-Felder genutzt. -- Für Import-Kompatibilität akzeptiert der Import weiterhin Payloads mit `extra` und mappt intern auf `profile`. +1. [Architektur im Überblick](#architektur-im-überblick) +2. [Verzeichnisstruktur](#verzeichnisstruktur) +3. [Profile](#profile) +4. [`ProfileHelper`-API](#profilehelper-api) +5. [`PluginRegistry` für Dritt-AddOns](#pluginregistry-für-dritt-addons) +6. [Custom-Plugins (`custom_plugins/*`)](#custom-plugins-custom_plugins) +7. [Build- und Asset-Pipeline](#build--und-asset-pipeline) +8. [Extension Points](#extension-points) +9. [APIs, Snippets, Style-Sets](#apis-snippets-style-sets) +10. [Layout Rules](#layout-rules) +11. [Mitwirken](#mitwirken) -## Standardprofile: zentrale JSON-Quelle +--- -Die Datei `install/tinymce-profiles.json` ist die Single Source of Truth für die AddOn-Standardprofile. +## Architektur im Überblick -Aktuelles Verhalten: +Das AddOn besteht aus vier Schichten: -- **Install** (`install.php`): importiert alle Profile aus `install/tinymce-profiles.json` (ohne bestehende Profile zu überschreiben). -- **Update** (`update.php`): importiert nur das Profil `demo` aus `install/tinymce-profiles.json` mit `forceUpdate=true`. -- **Reset** (`pages/settings.php` und `pages/profile_fixer.php`): importiert alle Standardprofile aus `install/tinymce-profiles.json` mit `forceUpdate=true`. +| Schicht | Aufgabe | Wesentliche Dateien | +| --- | --- | --- | +| **Backend (PHP)** | Profil-Verwaltung, Style-Sets, Snippets, Profile-Assistant, APIs | `pages/`, `lib/`, `boot.php` | +| **Provider-Layer** | Generiert die TinyMCE-Assets/Optionen für Frontend und Backend | `lib/TinyMce/Provider/` | +| **Frontend-JS** | `base.js` (Bootstrap), `profile_builder.js` (Assistant), Custom-Plugins | `assets/scripts/` | +| **Vendor-JS** | Upstream TinyMCE aus dem npm-Paket `tinymce` | `assets/vendor/tinymce/` | -Konsequenz: Standardprofile werden nicht mehr als große PHP-Strings im Code gepflegt. +Profile werden zentral in der Tabelle `rex_tinymce_profiles` gehalten, ausschließlich in der Spalte `profile` (YAML-ähnlicher Optionsblock). Über den `Provider`-Layer werden die Optionen samt globaler Erweiterungen (Plugins, Snippets, Style-Sets) zu TinyMCE-Initialisierungsdaten zusammengeführt und in `base.js` an `tinymce.init(...)` übergeben. -## Demo-Profil +## Verzeichnisstruktur -Kennung: `demo` (siehe `lib/TinyMce/Utils/DemoProfile.php`). +``` +tinymce/ +├── assets/ +│ ├── scripts/ # Eigene JS (base.js, profile_builder.js, …) +│ │ └── tinymce/plugins/ # Build-Output der Custom-Plugins +│ └── vendor/tinymce/ # NUR upstream TinyMCE (Core, Skins, Models, Core-Plugins) +├── boot.php # Registriert Provider, Extension Points, Plugins +├── custom_plugins/ # Quellen der mitgelieferten Plugins (for_*, cleanpaste, …) +│ └── / +│ ├── package.json # esbuild-Setup pro Plugin +│ ├── build.js +│ └── src/main/ts/ # TypeScript-Quelle +├── install/ +│ └── tinymce-profiles.json # Single Source of Truth für Standardprofile +├── lang/ # Sprachdateien +├── lib/ # Backend-PHP (Provider, Renderer, Utils, APIs) +├── pages/ # Backend-Seiten (profiles, settings, stylesets, …) +├── scripts/ # Build-Skripte (Node) +└── package.json # pnpm-Workspace für custom_plugins/* +``` -Backend-Verhalten in `pages/profiles.php`: +## Profile -- Bearbeiten des Demo-Profils ist erlaubt. -- Löschen des Demo-Profils ist gesperrt. -- Duplizieren des Demo-Profils ist erlaubt (als Startpunkt für eigene Profile). +### Datenmodell -## Profile für andere AddOns bereitstellen +- Tabelle: `rex_tinymce_profiles` +- Genutzte Spalten: `name`, `description`, `profile`, Standard-Metadaten +- Die Spalte `profile` enthält den vollständigen Optionsblock im `key: value`-Format (TinyMCE-Style) -Empfohlen ist ein JSON-basierter Workflow. +### Standardprofile -1. Profil im Backend erstellen und exportieren. -2. JSON in das eigene AddOn legen. -3. Beim Install/Update mit `ProfileHelper::importProfileFromJson()` einspielen. +`install/tinymce-profiles.json` ist die Single Source of Truth für die mitgelieferten Profile. -### Empfohlenes Beispiel (JSON-Import) +| Trigger | Verhalten | +| --- | --- | +| `install.php` | importiert alle Profile, **ohne** vorhandene zu überschreiben | +| `update.php` | importiert nur das Profil `demo` mit `forceUpdate = true` | +| Reset im Backend (`pages/settings.php`, `pages/profile_fixer.php`) | importiert alle Profile mit `forceUpdate = true` | -```php -if (rex_addon::get('tinymce')->isAvailable() && class_exists(\FriendsOfRedaxo\TinyMce\Utils\ProfileHelper::class)) { - $jsonFile = rex_path::addon('mein_addon', 'data/tinymce_profile.json'); +### Demo-Profil (`demo`) + +Implementierung: `lib/TinyMce/Utils/DemoProfile.php`. +- Bearbeiten: erlaubt +- Duplizieren: erlaubt (empfohlener Startpunkt für eigene Profile) +- Löschen: gesperrt (UI- und API-seitig) + +### Profile aus anderen AddOns einspielen + +Empfohlener Weg: JSON-Export aus dem Backend in das eigene AddOn legen und im `install.php`/`update.php` importieren. + +```php +if (rex_addon::get('tinymce')->isAvailable() + && class_exists(\FriendsOfRedaxo\TinyMce\Utils\ProfileHelper::class) +) { \FriendsOfRedaxo\TinyMce\Utils\ProfileHelper::importProfileFromJson( - $jsonFile, - false + rex_path::addon('mein_addon', 'data/tinymce_profile.json'), + false // forceUpdate ); } ``` -### Optional: nur bestimmte Profilnamen importieren +Optional auf bestimmte Profilnamen einschränken: ```php \FriendsOfRedaxo\TinyMce\Utils\ProfileHelper::importProfileFromJson( $jsonFile, true, - ['demo'] + ['mein_profil'] ); ``` -### Alternative: programmatisch via ensureProfile +Programmatische Variante ohne JSON-Datei: ```php \FriendsOfRedaxo\TinyMce\Utils\ProfileHelper::ensureProfile( @@ -75,492 +118,252 @@ if (rex_addon::get('tinymce')->isAvailable() && class_exists(\FriendsOfRedaxo\Ti ); ``` -## ProfileHelper API (aktuell) +## `ProfileHelper`-API -`FriendsOfRedaxo\TinyMce\Utils\ProfileHelper`: +Klasse: `FriendsOfRedaxo\TinyMce\Utils\ProfileHelper`. -- `importProfileFromJson(string $filePath, bool $forceUpdate = false, array $onlyNames = []): bool` - - Empfohlene Methode für JSON-Importe. - - Unterstützt Einzelprofil oder Profil-Array. - - Unterstützt Legacy-Feld `extra`. -- `ensureProfile(string $name, string $description, array $data = [], bool $forceUpdate = false): bool` -- `normalizeImportedProfile(array $profile): ?array` -- `ensureProfileFromImportedArray(array $profile, bool $forceUpdate = false): bool` -- `generateEnsureProfileCode(array $profile): string` +| Methode | Zweck | +| --- | --- | +| `importProfileFromJson(string $filePath, bool $forceUpdate = false, array $onlyNames = []): bool` | Empfohlener Einstieg. Akzeptiert Einzel- und Sammel-Exports. | +| `ensureProfile(string $name, string $description, array $data = [], bool $forceUpdate = false): bool` | Programmatisches Anlegen/Aktualisieren. | +| `normalizeImportedProfile(array $profile): ?array` | Normalisiert Import-Payloads für das interne Schema. | +| `ensureProfileFromImportedArray(array $profile, bool $forceUpdate = false): bool` | Schreibt ein normalisiertes Profil. | +| `generateEnsureProfileCode(array $profile): string` | Generiert ein PHP-Snippet für `ensureProfile()`-Aufrufe (z. B. für eigene Installer). | -Wichtig: Erfolgreiche Writes triggern intern `Profiles::profilesCreate()`. +Erfolgreiche Schreibvorgänge triggern intern `Profiles::profilesCreate()`, sodass Caches und abgeleitete Strukturen aktualisiert werden. -## Profil-Assistent und Protected Extras +## `PluginRegistry` für Dritt-AddOns -Der Profil-Assistent unterstützt weiterhin den Round-Trip mit `Protected Extras`. - -- Nicht direkt vom Assistenten verwaltete Optionen bleiben darüber erhalten. -- Extras werden beim Generieren wieder angehängt. - -## Eigene Plugins erstellen und über eigenes AddOn anmelden - -Der typische Integrationsweg besteht aus drei Schritten. - -1. Plugin-JS im eigenen AddOn bereitstellen (z. B. unter `assets/`). -2. Plugin in der `boot.php` des eigenen AddOns über `PluginRegistry::addPlugin()` registrieren. -3. Pluginname in einem TinyMCE-Profil aktivieren (`plugins: '...'`) und optional den Button in die Toolbar aufnehmen. - -### 1) Plugin-Datei erstellen (im eigenen AddOn) - -Minimalbeispiel für eine Plugin-Datei: - -```javascript -(function () { - 'use strict'; - - tinymce.PluginManager.add('mein_addon_plugin', function (editor) { - editor.ui.registry.addButton('mein_addon_button', { - text: 'Mein Button', - onAction: function () { - editor.insertContent('

Hallo aus meinem Plugin

'); - } - }); - }); -})(); -``` - -### 2) Registrierung in der boot.php des eigenen AddOns +Andere REDAXO-AddOns können eigene TinyMCE-Plugins beisteuern, ohne sie in dieses AddOn einzuchecken. Die Registrierung erfolgt in der `boot.php` des Fremd-AddOns. ```php -if (rex_addon::get('tinymce')->isAvailable() && class_exists(\FriendsOfRedaxo\TinyMce\PluginRegistry::class)) { +if (rex_addon::get('tinymce')->isAvailable() + && class_exists(\FriendsOfRedaxo\TinyMce\PluginRegistry::class) +) { \FriendsOfRedaxo\TinyMce\PluginRegistry::addPlugin( - 'mein_addon_plugin', - rex_url::addonAssets('mein_addon', 'mein_addon_plugin/plugin.min.js'), - 'mein_addon_button' + 'mein_addon_plugin', // Pluginname (eindeutig, präfixen!) + rex_url::addonAssets('mein_addon', 'mein_addon_plugin/plugin.min.js'), // URL zur JS-Datei + 'mein_addon_button' // optional: Toolbar-Button ); } ``` -Signatur von `addPlugin(...)`: - -- `$pluginName`: interner TinyMCE-Pluginname (muss eindeutig sein) -- `$pluginUrl`: URL zur Plugin-JS-Datei -- `$toolbarButton`: optionaler Toolbar-Buttonname - -### 3) Plugin im Profil aktivieren - -Die Registrierung macht ein Plugin verfügbar, aktiviert es aber nicht automatisch in jedem Profil. +Verhalten: -- Im Profil muss der Name in `plugins` enthalten sein. -- Falls gewünscht, muss der Button zusätzlich in der Toolbar vorkommen. +- `PluginRegistry` hängt automatisch einen Cache-Buster `?v=` an die URL. +- Der Eintrag landet als `external_plugins`-Eintrag in der TinyMCE-Init-Konfiguration. +- In `base.js` werden `external_plugins` gegen die `plugins`-Liste des aktiven Profils gefiltert — ein Plugin wird also nur initialisiert, wenn es im Profil auch tatsächlich aktiviert ist. +- Im Profil müssen Pluginname (`plugins: '… mein_addon_plugin'`) und optional Buttonname (`toolbar: '… mein_addon_button'`) ergänzt werden. -Beispiel im Profilblock: +Minimal-Plugin (JS): -```text -plugins: 'autolink link mein_addon_plugin', -toolbar: 'undo redo | bold italic | mein_addon_button' +```javascript +tinymce.PluginManager.add('mein_addon_plugin', function (editor) { + editor.ui.registry.addButton('mein_addon_button', { + text: 'Mein Button', + onAction: function () { + editor.insertContent('

Hallo

'); + } + }); +}); ``` -Wichtige Hinweise: - -- Nutze einen Präfix im Pluginnamen (z. B. `mein_addon_*`), um Kollisionen zu vermeiden. -- `PluginRegistry` hängt automatisch Cache-Busting (`?v=...`) an die Plugin-URL. -- Intern wird über den Extension Point `TINYMCE_PROFILE_OPTIONS` registriert. - -## Extension Points +Konventionen: -### TINYMCE_PROFILE_OPTIONS +- Pluginnamen immer mit AddOn-Präfix (`mein_addon_*`), um Kollisionen mit Core- und `for_*`-Plugins zu vermeiden. +- Kein Inline-JS in PHP-Strings — Plugins liegen als eigene Dateien im Asset-Baum des AddOns. -Erweitert verfügbare Profil-Optionen (Plugins, Toolbar-Buttons, externe Plugin-URLs). +## Custom-Plugins (`custom_plugins/*`) -### TINYMCE_GLOBAL_OPTIONS +Die mitgelieferten Custom-Plugins (`for_a11y`, `for_abbr`, `for_chars_symbols`, `for_checklist`, `for_footnotes`, `for_htmlembed`, `for_images`, `for_markdown`, `for_oembed`, `for_toc`, `for_video`, `cleanpaste`, `link_yform`, `mediapaste`, `phonelink`, `quote`, `snippets`) leben jeweils als eigenständige Pakete unter `custom_plugins/`. -Wird in `Provider\Assets::provideBaseAssets()` gefeuert. -Geeignet für globale Optionen wie zusätzliche `content_css` oder `style_formats`. +### Aufbau eines Custom-Plugins -### TINY_PROFILE_CLONE - -Wird nach dem Duplizieren eines Profils in der Backend-UI gefeuert. - -- Subject: neue Profil-ID (`int`) - -Beispiel: - -```php -rex_extension::register('TINY_PROFILE_CLONE', static function (rex_extension_point $ep) { - $newProfileId = (int) $ep->getSubject(); - rex_logger::factory()->log('info', 'Profil geklont: ' . $newProfileId); -}); ``` - -### TINY_PROFILE_DELETE - -Wird nach dem Löschen eines Profils gefeuert. - -- Subject: gelöschte Profil-ID (`int`) - -Beispiel: - -```php -rex_extension::register('TINY_PROFILE_DELETE', static function (rex_extension_point $ep) { - $deletedProfileId = (int) $ep->getSubject(); - // Eigene Cleanup-Logik -}); +custom_plugins// +├── package.json # Build-Konfig (esbuild) +├── build.js # Entry-Point-Build +├── src/main/ts/ +│ ├── Main.ts # Bindet Plugin.ts an `tinymce.PluginManager.add(...)` +│ └── Plugin.ts # Implementierung (Setup-Funktion) +└── dist// # Build-Output (plugin.js, plugin.min.js) ``` -## APIs im AddOn - -Vorhandene API-Klassen unter `lib/`: - -- `rex_api_tinymce_get_snippets` -- `rex_api_tinymce_media_upload` -- `rex_api_tinymce_media_categories` -- `rex_api_tinymce_media_meta` - -## Style-Sets - -- Datenquelle: `rex_tinymce_stylesets` -- Backend-Verwaltung: `pages/stylesets.php` -- Aktive Sets werden in `Provider\Assets::loadActiveStyleSets()` geladen und als globale TinyMCE-Optionen bereitgestellt. - -## Build und Assets - -Node-Skripte in `package.json`: - -- `pnpm run vendor:copy` -- `pnpm run plugins:build` -- `pnpm run build` (staging build + sync) -- `pnpm run clean-build` - -Relevante Skripte liegen unter `scripts/`: +### Wichtig: Bundling-Konvention -- `build-plugins.js` -- `vendor-copy.js` -- `sync-build-to-assets.js` -- `clean-build.js` +`Plugin.ts` exportiert ausschließlich die TinyMCE-Setup-Funktion (`export default (): void => { … }`). **Erst `Main.ts` ruft `tinymce.PluginManager.add(...)`** auf — daher muss `build.js` jedes Plugins `Main.ts` als Entry verwenden, nicht direkt `Plugin.ts`. Wird das ignoriert, registriert sich das Plugin zur Laufzeit nicht. -## Bei der Weiterentwicklung helfen +### Lokale Plugin-Entwicklung -Beiträge sind willkommen. Damit Änderungen gut reviewbar und stabil bleiben, halte dich an diese Leitplanken: - -- Arbeite mit kleinen, fokussierten Commits (ein Thema pro Commit). -- Bevorzuge Anpassungen in `install/tinymce-profiles.json` statt harter Profil-Strings in PHP. -- Nutze für neue Profil- oder Import-Logik immer `ProfileHelper`, nicht direkte DB-Spezialwege. -- Belasse Demo-Schutzregeln intakt (`demo` darf nicht löschbar werden). -- Registriere neue Plugins über `PluginRegistry::addPlugin()` statt über verstreute Einzel-Implementierungen. -- Lege JavaScript in Dateien unter `assets/scripts/` oder `custom_plugins/` ab, nicht inline in PHP. -- Achte auf Rückwärtskompatibilität bei Profil-Imports (`extra`-Legacy-Feld weiterhin akzeptieren). - -Empfohlene lokale Checks vor einem PR/Commit: - -1. PHP-Syntax prüfen (`php -l` auf geänderten Dateien). -2. Statische Analyse laufen lassen (`rexstan:analyze redaxo/src/addons/tinymce/`). -3. Bei JS- oder Plugin-Änderungen Build ausführen und erzeugte Assets prüfen. -4. Kurz im Backend testen: Profile-Liste, Preview, Import/Export, Reset, Demo-Schutz. - -## Build-Prozess im Detail - -Der Build besteht aus zwei Schritten: Vendor-Dateien bereitstellen und Custom-Plugins bauen/synchronisieren. - -### Typische Abläufe - -- Vollständiger Build: - - `pnpm run build` - - Führt intern `build:staging` und danach `build:sync` aus. -- Nur Plugins neu bauen: - - `pnpm run plugins:build` -- Nur Vendor-Dateien aktualisieren: - - `pnpm run vendor:copy` -- Aufräumen: - - `pnpm run clean-build` - -### Was die Skripte machen - -- `vendor-copy.js` - - kopiert TinyMCE-Vendor-Dateien in das AddOn-Asset-Ziel. -- `build-plugins.js` - - baut Plugins aus `custom_plugins/*` (minifiziert/bündelt). -- `sync-build-to-assets.js` - - synchronisiert Build-Ergebnisse in die endgültigen `assets/`-Ziele. -- `clean-build.js` - - entfernt Build-Artefakte für einen sauberen Neuaufbau. - -### Wann welcher Build nötig ist - -- Änderung in `custom_plugins/*`: - - mindestens `pnpm run plugins:build`, empfohlen `pnpm run build`. -- Änderung an Vendor-Versionen oder Vendor-Dateien: - - `pnpm run vendor:copy`, danach empfohlen `pnpm run build`. -- Nur PHP-Logik (ohne JS/Assets): - - kein vollständiger Node-Build zwingend, aber rexstan + kurzer Backend-Test. - -### CI-Hinweis - -Für CI ist der übliche Ablauf: - -- `pnpm install && pnpm run build` -- anschließend Artefakte unter `assets/scripts/tinymce/plugins` und `assets/vendor/tinymce/plugins` prüfen. - -## Best Practices für AddOn-Integratoren +```bash +# Einzelnes Plugin bauen +cd custom_plugins/for_images +node build.js +``` -- Nutze bevorzugt JSON-Importe über `ProfileHelper::importProfileFromJson()`. -- Prüfe vor Integrationscode immer `rex_addon::get('tinymce')->isAvailable()`. -- Verwende eindeutige Pluginnamen mit Präfix, um Kollisionen zu vermeiden. -- Verlasse dich bei Standardprofilen auf `install/tinymce-profiles.json` statt auf eigene DB-Manipulationen. +Der Output liegt in `dist//`. Für den Live-Test im AddOn müssen die Dateien zusätzlich in den Asset-Baum (`assets/scripts/tinymce/plugins//`) gespiegelt werden — das erledigt regulär `pnpm run build` (siehe nächster Abschnitt). -## Layout Rules (Strukturoptimierung) +## Build- und Asset-Pipeline -Layout Rules sind ein **nicht-invasives Content-Korrektur-System**, das automatisch häufige HTML-Struktur-Probleme korrigiert. Sie greifen stilschweigend ein und arbeiten mit dem Design-System (z.B. UIkit) zusammen. +Voraussetzung: `pnpm` ist installiert. Das AddOn ist ein pnpm-Workspace mit den Custom-Plugins als Sub-Pakete. -### Die 4 Regeln +### Skripte (`package.json`) -#### 1. `no_images_in_headings` — Bilder aus Überschriften verschieben +| Befehl | Wirkung | +| --- | --- | +| `pnpm install` | Installiert Abhängigkeiten und führt automatisch `vendor:copy` aus (`postinstall`). | +| `pnpm run vendor:copy` | Kopiert `node_modules/tinymce` → `assets/vendor/tinymce/`. | +| `pnpm run plugins:build` | Baut alle `custom_plugins/*` und legt die Ergebnisse in `assets/scripts/tinymce/plugins//` ab. | +| `pnpm run build` | Vollständiger Staged-Build: Vendor + Plugins werden zuerst nach `build/` gebaut, danach via `build:sync` in den Asset-Baum gespiegelt. | +| `pnpm run clean-build` | Räumt `build/` auf und entfernt versehentlich im Vendor-Baum gelandete Custom-Plugin-Ordner. | +| `pnpm run clean-plugins` | Löscht die Build-Ergebnisse der Custom-Plugins. | -**Was macht es:** -- Erkennt Bilder (``, `
`, ``), die direkt in Überschriften (h1–h6) sind -- Bewegt die Elemente automatisch **vor** die Überschrift (nicht invasiv, nur Repositionierung) +### Asset-Layout (verbindlich) -**Beispiel (vorher):** -```html -

- ... - Überschrift -

-``` +| Pfad | Inhalt | Eigentümer-Skript | +| --- | --- | --- | +| `assets/vendor/tinymce/` | **Nur** upstream TinyMCE (Core, Themes, Icons, Models, Skins, Core-Plugins) | `scripts/vendor-copy.js` | +| `assets/scripts/tinymce/plugins/` | Alle Custom-Plugins dieses AddOns | `scripts/build-plugins.js` (+ `sync-build-to-assets.js`) | +| `external_plugins` (extern) | Per `PluginRegistry::addPlugin()` von Fremd-AddOns angemeldete URLs | — | -**Beispiel (nachher):** -```html -... -

Überschrift

-``` +**Regel:** Niemals Custom-Plugins nach `assets/vendor/tinymce/plugins/` legen. Dieser Pfad wird bei jedem `vendor:copy` aus `node_modules` aufgefrischt — eigene Dateien dort gingen verloren bzw. blieben als veraltete Dubletten liegen. -**Use-Case:** Verhindert ungültige HTML-Struktur und visuelle Probleme in responsiven Designs. +### Skripte unter `scripts/` -#### 2. `collapse_empty_paragraphs` — Mehrfache Leerzeilen zusammenfassen +- `vendor-copy.js` — kopiert `node_modules/tinymce` in den Vendor-Baum (im Staging nach `build/vendor/tinymce/`). Berührt sonst nichts. +- `build-plugins.js` — iteriert über `custom_plugins/*`, ruft pro Plugin den lokalen Build auf und schreibt das Ergebnis ausschließlich in den Custom-Plugin-Asset-Pfad. Optional `--clean`, um alte Builds zu entfernen. +- `sync-build-to-assets.js` — spiegelt `build/vendor/tinymce/` → `assets/vendor/tinymce/` und `build/plugins/` → `assets/scripts/tinymce/plugins/`. Keine Cross-Kopien. +- `clean-build.js` — entfernt `build/` und löscht in `assets/vendor/tinymce/plugins/` alle Unterordner, deren Namen einem Eintrag in `custom_plugins/` entsprechen. -**Was macht es:** -- Erkennt 2+ hintereinander folgende leere `

`-Tags -- Ersetzt sie durch ein einzelnes `

` mit einer konfigurationsfähigen CSS-Klasse (z.B. `uk-margin`) +### CI-Build -**Beispiel (vorher):** -```html -

Text

-

-

-

Mehr Text

+```bash +pnpm install +pnpm run build ``` -**Beispiel (nachher, mit `uk-margin`):** -```html -

Text

-
-

Mehr Text

-``` - -**Use-Case:** -- Semantisch korrektere HTML-Struktur (keine leeren `

` mehr) -- Visuelle Konsistenz durch Design-System-Abstände - -**Konfiguration:** über `for_layout_rules_clear_element_class` (Standard: `uk-margin`) +Sanity-Check danach: -#### 3. `convert_lines_to_hr` — Minus-Linien in Trennlinien konvertieren - -**Was macht es:** -- Erkennt Minuszeichen-Reihen (`---`, `----`, etc.) in `

`-Tags -- Ersetzt sie durch semantische `


`-Elemente mit konfigurierter CSS-Klasse (z.B. `uk-divider-icon`) - -**Beispiel (vorher):** -```html -

Text

-

----

-

Mehr Text

+```bash +ls assets/vendor/tinymce/plugins/ # darf KEINE Namen aus custom_plugins/ enthalten +ls assets/scripts/tinymce/plugins/ # muss alle Custom-Plugins enthalten ``` -**Beispiel (nachher, mit `uk-divider-icon`):** -```html -

Text

-
-

Mehr Text

-``` +## Extension Points -**Use-Case:** -- Typografische Konvention: Editor-Nutzer tippen `----` und bekommen ein semantisches HTML-Element -- Verknüpfung mit Design-System für konsistente Visualisierung +### `TINYMCE_PROFILE_OPTIONS` -**Konfiguration:** über `for_layout_rules_hr_class` (Standard: `uk-divider-icon`) +Erweitert die im Profil-Assistant verfügbaren Plugins, Toolbar-Buttons und externen Plugin-URLs. Wird von `PluginRegistry` selbst genutzt, kann aber auch direkt angesteuert werden. -#### 4. (Implizit) — Leere Paragraphen am Anfang/Ende entfernen +### `TINYMCE_GLOBAL_OPTIONS` -**Was macht es:** -- Räumt einzelne leere `

`-Tags am Anfang und Ende des Dokuments auf -- Teil der `applyLayoutRules()`-Logik, aber separat von Regel 2 +Wird in `Provider\Assets::provideBaseAssets()` gefeuert. Geeignet für globale Optionen wie zusätzliche `content_css`, `style_formats`, `font_family_formats` etc. -**Use-Case:** Vermeidung von unnötigen Whitespace-Elementen nach Paste/Import. +### `TINY_PROFILE_CLONE` -### Konfiguration in TinyMCE-Profilen +Wird nach dem Duplizieren eines Profils im Backend gefeuert. -Layout Rules werden über Profil-Optionen aktiviert: +- Subject: neue Profil-ID (`int`) -```javascript -// Komplettes Beispiel -for_layout_rules_no_images_in_headings: true, -for_layout_rules_collapse_empty_paragraphs: true, -for_layout_rules_convert_lines_to_hr: true, -for_layout_rules_clear_element_class: 'uk-margin', -for_layout_rules_hr_class: 'uk-divider-icon' +```php +rex_extension::register('TINY_PROFILE_CLONE', static function (rex_extension_point $ep) { + $newProfileId = (int) $ep->getSubject(); + // … +}); ``` -**Hinweis:** Alle Layout Rules sind **optional**. Wenn nicht angegeben, gelten Defaults: -- `no_images_in_headings`: immer aktiv (Basis-Struktur-Validität) -- `collapse_empty_paragraphs`: false (nur in Standard/Full Presets aktiv) -- `convert_lines_to_hr`: false (optional, muss explizit aktiviert werden) -- `clear_element_class`: `'uk-margin'` (Standard UIkit-Klasse) -- `hr_class`: `'uk-divider-icon'` (Standard UIkit-Klasse) - -### Profile Assistant Integration - -Der Profile Assistant bietet eine **benutzerfreundliche Checkbox-Form** für Layout Rules: - -1. **Reiter "Erweiterte Optionen"** → Abschnitt "Layout-Regeln" -2. Vier Checkboxen für Regel-Aktivierung -3. Zwei Textfelder für CSS-Klassen +### `TINY_PROFILE_DELETE` -#### Intelligente Presets +Wird nach dem Löschen eines Profils gefeuert. -Die Presets stellen sinnvolle Defaults ein: +- Subject: gelöschte Profil-ID (`int`) -| Preset | `no_images` | `collapse_empty` | `convert_lines` | Clear-Klasse | HR-Klasse | -|--------|-----------|-----------------|-----------------|-------------|-----------| -| **Simple** | ✓ | – | – | `uk-margin` | `uk-divider-icon` | -| **Standard** | ✓ | ✓ | ✓ | `uk-margin` | `uk-divider-icon` | -| **Full** | ✓ | ✓ | ✓ | `uk-margin-medium` | `uk-divider-icon` | +## APIs, Snippets, Style-Sets -- **Simple:** Nur Basis-Struktur-Sicherheit (Bilder aus Headings) -- **Standard:** Ausbalanciert für typische Content-Anforderungen -- **Full:** Maximale Korrektur, größere vertikale Abstände via `uk-margin-medium` +### `rex_api_function`-Klassen unter `lib/` -### Implementierungsdetails +| API | Zweck | +| --- | --- | +| `rex_api_tinymce_get_snippets` | Liefert die im Backend gepflegten Snippets als JSON. | +| `rex_api_tinymce_media_upload` | Direkt-Upload aus dem Editor in den Medienpool. | +| `rex_api_tinymce_media_categories` | Listet Medienpool-Kategorien für Pickers. | +| `rex_api_tinymce_media_meta` | Liefert Metadaten zu einem Medienpool-Eintrag. | -#### Location: `assets/scripts/base.js` +### Snippets -```javascript -// ~Zeile 841-930 - -// Layout Rules Konfiguration -var layoutRules = { - no_images_in_headings: options.for_layout_rules_no_images_in_headings ?? true, - collapse_empty_paragraphs: options.for_layout_rules_collapse_empty_paragraphs ?? false, - clear_element_class: options.for_layout_rules_clear_element_class ?? 'uk-margin', - convert_lines_to_hr: options.for_layout_rules_convert_lines_to_hr ?? false, - hr_class: options.for_layout_rules_hr_class ?? 'uk-divider-icon' -}; - -// Hauptfunktion: applyLayoutRules(html) -// - Nutzt DOMParser für robuste DOM-Manipulation -// - Regel 1: Bilder vor h1–h6 verschieben -// - Regel 2: Mehrfache leere

→ Clear-Div -// - Regel 3: Minus-Reihen →


-// - Regel 4: Leere

am Anfang/Ende entfernen -``` - -**Event-Handler:** -```javascript -editor.on('SetContent BeforeSetContent', function(e) { - var modified = applyLayoutRules(e.content); - if (modified) { - e.content = modified; - } -}); -``` +- Datenquelle: Backend-Pflegeoberfläche +- Auslieferung: `rex_api_tinymce_get_snippets` (vom `snippets`-Plugin konsumiert) -Regeln gelten beim: -- **Laden** von Inhalt (SetContent, BeforeSetContent) -- **Einfügen** von Content (Paste) -- **Speichern** (transparent für Frontend) +### Style-Sets -#### Profile Assistant Integration +- Datenquelle: `rex_tinymce_stylesets` +- Backend-Verwaltung: `pages/stylesets.php` +- Aktive Sets werden in `Provider\Assets::loadActiveStyleSets()` geladen und global als `style_formats`/`content_style` an TinyMCE übergeben. -Location: `redaxo/src/addons/tinymce/assets/scripts/profile_builder.js` +## Layout Rules -- **UI-Form:** Checkboxen und Textfelder nach Image-Width-Legende (~Zeile 249–263) -- **generateConfig():** Schreibt Layout-Rule-Optionen in Textarea (~Zeile 2730–2754) -- **loadFromConfig():** Parst Layout-Rule-Optionen aus Konfig (~Zeile 3125–3145) -- **MANAGED_PROFILE_KEYS:** Registriert 6 neue Keys für Tracking +Layout Rules sind ein nicht-invasives Korrektursystem im `base.js`, das beim Setzen oder Einfügen von Inhalt typische Strukturprobleme glättet. Sie werden pro Profil über Optionen aktiviert. -#### Sprachstrings +### Regeln -**Deutsch (`lang/de_de.lang`):** -``` -tinymce_layout_rules = Layout-Regeln (Strukturoptimierung) -tinymce_layout_rules_help = Automatische Korrektur von häufigen Content-Struktur-Problemen. -tinymce_layout_no_images_in_headings = Bilder aus Überschriften verschieben -tinymce_layout_no_images_in_headings_help = Bilder in h1-h6 werden davor platziert. -tinymce_layout_collapse_empty = Mehrfache Leerzeilen zusammenfassen -tinymce_layout_collapse_empty_help = Mehrere hintereinander folgende leere

werden durch ein Clear-Element ersetzt. -tinymce_layout_delete_empty = Einzelne leere Absätze löschen -tinymce_layout_delete_empty_help = Entfernt einzelne leere

am Anfang und Ende. -tinymce_layout_lines_to_hr = Minus-Linien → Trennlinie (----) -tinymce_layout_lines_to_hr_help = Mehrere Minuszeichen werden zu


umgewandelt. -tinymce_layout_clear_element = Clear-Element CSS-Klasse -tinymce_layout_hr_class = Trennlinien CSS-Klasse -``` +| Schlüssel | Wirkung | +| --- | --- | +| `for_layout_rules_no_images_in_headings` | Bilder (`img`/`figure`/`picture`) innerhalb `h1`–`h6` werden vor die Überschrift verschoben. | +| `for_layout_rules_collapse_empty_paragraphs` | ≥ 2 aufeinanderfolgende leere `

` werden zu einem `

` mit der konfigurierten Clear-Klasse zusammengefasst. | +| `for_layout_rules_convert_lines_to_hr` | Reihen aus Minuszeichen (`---`, `----`, …) in einem `

` werden zu `


` mit konfigurierter Klasse. | +| (implizit) | Einzelne leere `

` am Anfang/Ende des Dokuments werden entfernt. | -**Englisch (`lang/en_gb.lang`):** -``` -tinymce_layout_rules = Layout rules (structure optimization) -tinymce_layout_rules_help = Automatic correction of common content structure issues. -tinymce_layout_no_images_in_headings = Move images out of headings -tinymce_layout_no_images_in_headings_help = Images inside h1-h6 are moved before the heading. -tinymce_layout_collapse_empty = Collapse multiple empty lines -tinymce_layout_collapse_empty_help = Multiple consecutive empty

tags are replaced with a clear element. -tinymce_layout_delete_empty = Delete single empty paragraphs -tinymce_layout_delete_empty_help = Removes single empty

tags at the beginning and end. -tinymce_layout_lines_to_hr = Convert minus lines to horizontal rule (----) -tinymce_layout_lines_to_hr_help = Multiple minus signs are converted to


tags. -tinymce_layout_clear_element = Clear element CSS class -tinymce_layout_hr_class = Horizontal rule CSS class -``` +### Konfigurations-Optionen -### Verwendungsbeispiele für AddOn-Entwickler +| Option | Standard | +| --- | --- | +| `for_layout_rules_no_images_in_headings` | `true` | +| `for_layout_rules_collapse_empty_paragraphs` | `false` | +| `for_layout_rules_convert_lines_to_hr` | `false` | +| `for_layout_rules_clear_element_class` | `uk-margin` | +| `for_layout_rules_hr_class` | `uk-divider-icon` | -#### Minimales Profil mit Layout Rules +### Beispielprofil -```json -{ - "name": "mein_addon_minimal", - "description": "Minimalprofil mit Struktur-Sicherheit", - "profile": "plugins: 'autolink lists link'\ntoolbar: 'undo redo | bold italic'\nfor_layout_rules_no_images_in_headings: true" -} +```text +plugins: 'autolink lists link' +toolbar: 'undo redo | bold italic' +for_layout_rules_no_images_in_headings: true +for_layout_rules_collapse_empty_paragraphs: true +for_layout_rules_convert_lines_to_hr: true +for_layout_rules_clear_element_class: 'uk-margin' +for_layout_rules_hr_class: 'uk-divider-icon' ``` -#### Standard-Profil mit allen Rules +Für nicht-UIkit-Setups einfach die beiden Klassen-Optionen auf das eigene Design-System mappen (z. B. Bootstrap: `my-spacer`/`my-divider`). -```json -{ - "name": "mein_addon_standard", - "description": "Vollständiges Profil mit Layout-Korrektur", - "profile": "plugins: 'autolink lists link'\ntoolbar: 'undo redo | bold italic'\nfor_layout_rules_no_images_in_headings: true\nfor_layout_rules_collapse_empty_paragraphs: true\nfor_layout_rules_convert_lines_to_hr: true\nfor_layout_rules_clear_element_class: 'uk-margin'\nfor_layout_rules_hr_class: 'uk-divider-icon'" -} -``` +### Profile-Assistant -#### Custom Design-System Integration +Der Profile-Assistant zeigt Layout Rules unter „Erweiterte Optionen → Layout-Regeln“ als Checkbox-Form mit zwei Textfeldern für die CSS-Klassen. Die mitgelieferten Presets (Simple / Standard / Full) setzen sinnvolle Default-Kombinationen. -Falls dein AddOn ein anderes Design-System nutzt (z.B. Bootstrap): +## Mitwirken -```json -{ - "profile": "...\nfor_layout_rules_clear_element_class: 'my-spacer'\nfor_layout_rules_hr_class: 'my-divider'" -} -``` +Beiträge sind willkommen. Bitte halte dich an die folgenden Leitplanken — sie machen Reviews schnell und Releases stabil. -### FAQ +### Stilregeln -**F: Werden Layout Rules automatisch für alle Profile aktiviert?** -A: Nein. Nur `no_images_in_headings` ist Basis-Default. Andere Rules müssen explizit aktiviert werden. Presets stellen sinnvolle Defaults bereit. +- Kleine, fokussierte Commits (ein Thema pro Commit). +- Standardprofile **ausschließlich** in `install/tinymce-profiles.json` pflegen, nie als PHP-String inline. +- Profil-/Import-Logik immer über `ProfileHelper` führen, nicht über direkte SQL-Manipulation. +- Demo-Schutzregeln (`demo` nicht löschbar) intakt lassen. +- Dritt-Plugins **ausschließlich** via `PluginRegistry::addPlugin()` registrieren. +- JavaScript gehört in Dateien unter `assets/scripts/` oder `custom_plugins/`, niemals als Inline-String in PHP. +- Custom-Plugins niemals nach `assets/vendor/tinymce/` legen. -**F: Kann ich die Standard-CSS-Klassen überschreiben?** -A: Ja, über `for_layout_rules_clear_element_class` und `for_layout_rules_hr_class`. +### Lokale Checks vor einem PR -**F: Was passiert mit Protected Extras?** -A: Layout Rules beeinflussen die Protected Extras nicht. Sie werden nach Regelanwendung wieder angehängt. +1. PHP-Syntax: `find redaxo/src/addons/tinymce -name "*.php" -print0 | xargs -0 -n1 php -l` +2. Statische Analyse: `php redaxo/bin/console rexstan:analyze redaxo/src/addons/tinymce/` +3. Bei JS-/Plugin-Änderungen: `pnpm run build` ausführen und prüfen, dass nur erwartete Dateien geändert wurden. +4. Im Backend smoke-testen: Profile-Liste, Preview, Import/Export, Reset, Demo-Schutz, Profile-Assistant. -**F: Wie teste ich Layout Rules lokal?** -A: Profil mit Rules aktivieren → Backend öffnen → Content mit "problematischen" Strukturen (Bilder in Headings, mehrfache `

`, Minus-Reihen) einfügen → Ausgabe prüfen. +### Release-relevante Dateien -**F: Können Layout Rules beschädigt werden?** -A: Nein. Sie arbeiten rein strukturell (Bewegung, Ersetzung) und ändern Textinhalte nicht. +- `package.yml` (Version) +- `CHANGELOG.md` (Eintrag pro Release) +- `install/tinymce-profiles.json` (bei Profil-Änderungen am `demo`-Profil) diff --git a/FOR_PLUGINS.md b/FOR_PLUGINS.md index 04591651..15f641da 100644 --- a/FOR_PLUGINS.md +++ b/FOR_PLUGINS.md @@ -1,318 +1,544 @@ # FriendsOfREDAXO-Plugins für TinyMCE -Dieses AddOn liefert neben den Standard-Plugins von TinyMCE eine Reihe eigener **FOR-Plugins** (`for_*`). Sie sind open-source, speziell für REDAXO optimiert und decken Funktionen ab, für die sonst teils kommerzielle TinyMCE-Premium-Plugins nötig wären. +Dieses AddOn liefert neben den Standard-Plugins von TinyMCE eine Reihe eigener **FOR-Plugins** (`for_*`). Sie sind Open Source, speziell für REDAXO optimiert und decken Funktionen ab, für die sonst teils kommerzielle TinyMCE-Premium-Plugins oder externe Editor-Frameworks (wie CKEditor 5) nötig wären. + +Im **Profil-Assistenten** erscheinen alle Plugins dieses AddOns mit einem farbigen **Badge** (orange = FOR, blau = REDAXO/Klassiker) und sind in Plugin-Liste, Toolbar-Buttons und Custom-Menu-Items optisch hervorgehoben. + +> Diese Datei beschreibt die **mitgelieferten Plugins im Detail**. Für die allgemeine Bedienung (Profile, Style-Sets, Snippets, Konfiguration) siehe die [README.md](README.md). Für Entwickler-Details (Build, Extension Points, eigene Plugins registrieren) siehe [DEVS.md](DEVS.md). + +## Gruppen-Übersicht + +| Gruppe | Plugins | +| --- | --- | +| Medien & Einbettung | `for_images`, `for_oembed`, `for_video`, `mediapaste` | +| Struktur & Navigation | `for_toc`, `for_footnote` | +| Listen, Embeds & Content | `for_checklist`, `for_checklist_feature`, `for_htmlembed`, `for_markdown` | +| Sprache & Typografie | `for_chars_symbols`, `for_abbr`, `quote`, `phonelink` | +| Barrierefreiheit | `for_a11y` | +| Paste-Pipeline | `cleanpaste`, `mediapaste` | +| REDAXO-Integration | `snippets`, `link_yform` | + +| Plugin | Zweck | Toolbar/Menü | +| --- | --- | --- | +| `for_images` | Bilder als Figure-Workflow (Mediapool-Show/Swap, Presets, Caption) – zusammen mit Core `image` | `for_images` | +| `for_oembed` | YouTube-/Vimeo-Einbettung mit Live-Preview, **CKE5-kompatibles Save-Format** `
` | `for_oembed` | +| `for_video` | Lokale Videos aus dem Mediapool (HTML5 `
-``` +Demo-Sets für UIkit 3 und Bootstrap 5 lassen sich auf Knopfdruck installieren. JSON-Export/-Import wird unterstützt. -## Troubleshooting / Häufige Probleme +### Snippets -### Quickbar oder andere Einstellungen werden nicht übernommen +**TinyMCE → Snippets** verwaltet wiederverwendbare HTML-Bausteine. Das Plugin `snippets` macht sie im Editor (Standard im *Einfügen*-Menü) verfügbar. -**Symptom:** Die Quickbar (oder andere über den Profil-Assistenten konfigurierte Optionen wie `plugins`, `toolbar`) wird im Editor immer auf die TinyMCE-Standardwerte zurückgesetzt, obwohl sie im Profil gespeichert wurde. +### Paste-Pipeline (`cleanpaste`) -**Ursache:** In aktuellen Versionen liegt die Profilkonfiguration ausschließlich in der Spalte `profile`. Probleme entstehen meist durch veraltete Profile/Exporte aus früheren Versionen oder gecachte `profiles.js`-Dateien. +Zentrale Konfiguration unter **TinyMCE → Paste-Einstellungen**. Erlaubt: -**Lösung:** +- Tag-/Klassen-/Style-/ID-/`data-*`-Whitelists (auch Regex, z. B. `^uk-.*`) +- Cleanup-Stufen: BR-Reduktion, Leer-Paragraph-Entfernung, Office-/Word-Strip, DOM-Cleanup +- Schutz interner FOR-Markup-Konventionen (immer behalten: `for-*`-Klassen, `data-for-*`-Attribute, `media`-Klasse) -1. AddOn mindestens auf `v8.9.0` aktualisieren (Updatepfad setzt `v8.8.1` voraus). -2. Profil speichern oder `profiles.js` neu generieren, damit die aktuelle Konfiguration geschrieben wird. -3. Bei JSON-Import alte Exporte mit `extra` sind weiterhin nutzbar; sie werden beim Import automatisch nach `profile` migriert. +### Layout Rules (Strukturoptimierung) ---- +Vier nicht-invasive Korrekturen, pro Profil aktivierbar: -### Profil-Änderungen werden im Editor nicht sichtbar +| Option | Wirkung | +| --- | --- | +| `for_layout_rules_no_images_in_headings` | Bilder aus `h1`–`h6` davor platzieren | +| `for_layout_rules_collapse_empty_paragraphs` | ≥ 2 leere `

` → `

` mit Clear-Klasse | +| `for_layout_rules_convert_lines_to_hr` | `----` in `

` → `


` mit Klasse | +| (implizit) | Leere `

` am Anfang/Ende entfernen | -Nach dem Speichern eines Profils muss der Browser-Cache geleert werden (Strg+Shift+R / Cmd+Shift+R), da `profiles.js` als statische Datei ausgeliefert wird. +Default-Klassen: `uk-margin` (Clear) und `uk-divider-icon` (HR) – pro Profil überschreibbar. ---- +## Weiterführende Dokumentation -### TinyMCE zeigt eine Lizenzwarnung +| Thema | Datei | +| --- | --- | +| Detail-Beschreibung aller mitgelieferten Plugins (FOR & Klassiker) | [FOR_PLUGINS.md](FOR_PLUGINS.md) | +| Entwickler-Doku: Architektur, Build, Extension Points, `PluginRegistry`, `ProfileHelper` | [DEVS.md](DEVS.md) | +| Backend-Hilfe | Reiter **„Entwickler"** in der AddOn-Verwaltung | +| TinyMCE 8 Referenz | [tiny.cloud/docs/tinymce/latest](https://www.tiny.cloud/docs/tinymce/latest/) | -Jedes Profil muss `license_key: 'gpl',` in der Konfiguration enthalten. Bei eigenen Profilen diesen Eintrag am Anfang des `profile`-Feldes ergänzen. Der **Profil-Fixer** prüft unter **„TinyMCE 8 – Profil-Migration & Kompatibilitätsprüfung"** alle Profile auf diesen und weitere Kompatibilitätsprobleme. +## Troubleshooting (Kurz) ---- +| Symptom | Lösung | +| --- | --- | +| Profil-Änderungen werden im Editor nicht sichtbar | Browser-Cache leeren (Strg/⌘ + Shift + R) – `profiles.js` ist statisch. | +| TinyMCE-Lizenzwarnung | `license_key: 'gpl',` ergänzen, *Profil-Fixer* im Backend ausführen. | +| Quickbar / Plugins werden auf Defaults zurückgesetzt | AddOn auf ≥ `v8.9.0` aktualisieren (Updatepfad: `v8.8.1` → `v8.9.0`), Profil neu speichern. | +| SVG-Upload (`mediapaste`) schlägt fehl | REDAXO-Mediapool: **Erlaubte Dateitypen** prüfen. SVG ist aus Sicherheitsgründen oft nicht freigegeben. | +| Bild-Skalierung greift nicht | Media-Manager-Type `tiny` im Backend anlegen. | -## Entwickler +## Mitwirken -Informationen zur Erweiterung des Addons, zur Registrierung eigener Plugins, -zum Build-Prozess und zu Extension Points finden Sie in der -[Entwickler-Dokumentation](DEVS.md) oder im Backend unter dem Reiter "Entwickler". +Issues, Pull Requests und Plugin-Ideen sind willkommen. Beim Beisteuern bitte die Hinweise in [DEVS.md](DEVS.md) beachten (Stilregeln, Build-Layout, Tests). -## Licenses +## Lizenzen -- AddOn: [MIT LICENSE](https://github.com/FriendsOfREDAXO/tinymce/blob/master/LICENSE.md) – siehe [LICENSE.md](LICENSE.md) -- TinyMCE: [GPL v2+ LICENSE](https://github.com/tinymce/tinymce/blob/develop/license.md) (ab Version 8.0) – gebündelt unter [assets/vendor/tinymce/license.md](assets/vendor/tinymce/license.md), Drittanbieter-Hinweise unter [assets/vendor/tinymce/notices.txt](assets/vendor/tinymce/notices.txt) -- Eigene Custom-Plugins (`custom_plugins/*`): MIT (als Teil dieses AddOns) +| Komponente | Lizenz | +| --- | --- | +| Dieses AddOn (Core + Custom-Plugins `custom_plugins/*`) | [MIT](LICENSE.md) | +| TinyMCE (ab v8) | [GPL v2+](assets/vendor/tinymce/license.md), Drittanbieter-Hinweise in [`notices.txt`](assets/vendor/tinymce/notices.txt) | +| markdown-it (`for_markdown`) | MIT (gebündelt) | -Für kommerzielle Nutzung ohne GPL bietet Tiny kostenpflichtige Lizenzen an – siehe [tiny.cloud](https://www.tiny.cloud/). In diesem AddOn wird standardmäßig `license_key: 'gpl'` gesetzt. +Für kommerzielle, nicht-GPL-kompatible Nutzung bietet Tiny eigene Lizenzen an – siehe [tiny.cloud](https://www.tiny.cloud/). Standardmäßig wird hier `license_key: 'gpl'` gesetzt. -## Author +## Credits **Friends Of REDAXO** -- http://www.redaxo.org -- https://github.com/FriendsOfREDAXO +- Website: +- GitHub: +- AddOn-Repo: + +Mit Dank an alle Beitragenden, an die Tiny-Community für TinyMCE selbst und an die zahlreichen Open-Source-Projekte, auf denen die FOR-Plugins aufbauen (u. a. [markdown-it](https://github.com/markdown-it/markdown-it)). diff --git a/assets/css/profile_builder.css b/assets/css/profile_builder.css new file mode 100644 index 00000000..5ebf9ba6 --- /dev/null +++ b/assets/css/profile_builder.css @@ -0,0 +1,147 @@ +/* + * TinyMCE Profile Builder – Assistant-spezifisches Styling. + * + * Hauptabschnitte (`builder-group-title`) werden als farbige Balken dargestellt, + * damit die fünf Schritte klar voneinander getrennt sind. Untertitel + * (`builder-subsection-title`) bleiben kleiner, mit dezenter Border-bottom. + * + * Light + Dark (explicit) + Auto-Dark gem. REDAXO-Recipe. + */ + +.builder-group-title, +.builder-subsection-title { + --tmce-builder-bar-bg: #2980b9; + --tmce-builder-bar-fg: #ffffff; + --tmce-builder-bar-shadow: rgba(41, 128, 185, 0.18); + --tmce-builder-subtitle-fg: #2c3e50; + --tmce-builder-subtitle-border: #dce4ec; + --tmce-builder-subtitle-icon: #2980b9; +} + +body.rex-theme-dark .builder-group-title, +body.rex-theme-dark .builder-subsection-title { + --tmce-builder-bar-bg: #1f6391; + --tmce-builder-bar-fg: #f4f8fb; + --tmce-builder-bar-shadow: rgba(0, 0, 0, 0.45); + --tmce-builder-subtitle-fg: #e8f0f7; + --tmce-builder-subtitle-border: #3a4654; + --tmce-builder-subtitle-icon: #5ea9d9; +} + +@media (prefers-color-scheme: dark) { + body.rex-has-theme:not(.rex-theme-light) .builder-group-title, + body.rex-has-theme:not(.rex-theme-light) .builder-subsection-title { + --tmce-builder-bar-bg: #1f6391; + --tmce-builder-bar-fg: #f4f8fb; + --tmce-builder-bar-shadow: rgba(0, 0, 0, 0.45); + --tmce-builder-subtitle-fg: #e8f0f7; + --tmce-builder-subtitle-border: #3a4654; + --tmce-builder-subtitle-icon: #5ea9d9; + } +} + +/* Hauptabschnitt-Balken (Schritt 1..5) */ +.builder-group-title { + display: flex; + align-items: center; + gap: 10px; + margin: 32px 0 18px; + padding: 12px 18px; + background: var(--tmce-builder-bar-bg); + color: var(--tmce-builder-bar-fg); + border-radius: 4px; + box-shadow: 0 2px 4px var(--tmce-builder-bar-shadow); + font-size: 18px; + font-weight: 600; + line-height: 1.2; + text-transform: none; + border: 0; +} + +.builder-group-title .rex-icon { + font-size: 20px; + line-height: 1; + opacity: 0.9; +} + +.builder-group-title .builder-group-num { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 26px; + height: 26px; + padding: 0 8px; + margin-right: 4px; + background: rgba(255, 255, 255, 0.18); + border-radius: 13px; + font-size: 13px; + font-weight: 700; +} + +/* Erstes Balken-Element direkt am Container-Anfang nicht doppelt nach oben distanzieren */ +.builder-group-title:first-child { + margin-top: 8px; +} + +/* Untertitel-Legends innerhalb eines Abschnitts */ +.builder-subsection-title { + margin: 24px 0 10px; + padding: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: var(--tmce-builder-subtitle-fg); + border-bottom: 1px solid var(--tmce-builder-subtitle-border); + line-height: 1.4; + width: 100%; +} + +.builder-subsection-title .rex-icon { + color: var(--tmce-builder-subtitle-icon); + margin-right: 6px; +} + +/* ------------------------------------------------------------------ */ +/* Kompakte Form-Darstellung im Assistant */ +/* ------------------------------------------------------------------ */ + +#tinymce-profile-assistant .form-group { + margin-bottom: 10px; +} + +#tinymce-profile-assistant .form-group label { + margin-bottom: 4px; + font-weight: 600; +} + +#tinymce-profile-assistant .help-block { + font-size: 12px; + line-height: 1.4; + margin-top: 4px; + margin-bottom: 0; + color: #6c757d; +} + +body.rex-theme-dark #tinymce-profile-assistant .help-block { + color: #9aa6b2; +} + +@media (prefers-color-scheme: dark) { + body.rex-has-theme:not(.rex-theme-light) #tinymce-profile-assistant .help-block { + color: #9aa6b2; + } +} + +#tinymce-profile-assistant .checkbox { + margin-top: 6px; + margin-bottom: 6px; +} + +/* Checkbox in derselben Reihe wie Inputs – an Input-Baseline ausrichten */ +#tinymce-profile-assistant .builder-checkbox-align { + margin-top: 26px; +} + +/* Row-Abstände im Assistant etwas straffen */ +#tinymce-profile-assistant .row + .row { + margin-top: 6px; +} diff --git a/assets/images/glyph-tinymce@2x.png b/assets/images/glyph-tinymce@2x.png deleted file mode 100644 index f9b18ceb..00000000 Binary files a/assets/images/glyph-tinymce@2x.png and /dev/null differ diff --git a/assets/images/header-john-reed-qtRFE7MYnHo-unsplash.jpg b/assets/images/header-john-reed-qtRFE7MYnHo-unsplash.jpg deleted file mode 100644 index 0951505c..00000000 Binary files a/assets/images/header-john-reed-qtRFE7MYnHo-unsplash.jpg and /dev/null differ diff --git a/assets/images/tinymce_white.svg b/assets/images/tinymce_white.svg deleted file mode 100644 index 6988a4c7..00000000 --- a/assets/images/tinymce_white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/scripts/base.js b/assets/scripts/base.js index 8e6a0660..14fcfc66 100644 --- a/assets/scripts/base.js +++ b/assets/scripts/base.js @@ -853,6 +853,24 @@ function tiny_init(container) { activePlugins = options.plugins.split(/\s+/).filter(Boolean); } + // Issue #171: external_plugins entries are loaded AND initialized by TinyMCE + // regardless of the `plugins:` list. Without filtering, every registered custom + // plugin (e.g. for_images) would activate in every profile and add its buttons/ + // toolbars even when the profile explicitly disabled it. Drop entries that are + // not part of the profile's active plugin list. + if (options.external_plugins && typeof options.external_plugins === 'object') { + let filteredExternal = {}; + for (let extName in options.external_plugins) { + if (!Object.prototype.hasOwnProperty.call(options.external_plugins, extName)) { + continue; + } + if (activePlugins.indexOf(extName) !== -1) { + filteredExternal[extName] = options.external_plugins[extName]; + } + } + options.external_plugins = filteredExternal; + } + if (activePlugins.indexOf('quickbars') === -1) { delete options.quickbars_selection_toolbar; delete options.quickbars_insert_toolbar; diff --git a/assets/scripts/profile_builder.js b/assets/scripts/profile_builder.js index 14ba3742..1858394a 100644 --- a/assets/scripts/profile_builder.js +++ b/assets/scripts/profile_builder.js @@ -14,28 +14,15 @@ function initTinyMceProfileAssistant() { $builderContainer.append($builderHeader); $builderContainer.append($builderBody); - // Toggle Button + // Toggle Button – die bestehende Konfiguration wird beim Init automatisch + // in den Assistenten geladen (siehe loadFromConfig-Aufruf am Ende), daher + // gibt es keinen separaten "Konfiguration übernehmen"-Button mehr. const $toggleBtn = $(''); $toggleBtn.on('click', function() { $builderContainer.slideToggle(); }); - // Load-from-config Button (populates the builder with the current textarea config) - const $loadBtn = $(''); - $loadBtn.on('click', function() { - const loaded = loadFromConfig($textarea, $builderBody); - if (loaded) { - if (!$builderContainer.is(':visible')) { - $builderContainer.slideDown(); - } - alert(i18n.loaded_from_config || 'Configuration loaded into the assistant.'); - } else { - alert(i18n.load_failed || 'Could not parse configuration.'); - } - }); - $textarea.before($toggleBtn); - $textarea.before($loadBtn); $textarea.before($builderContainer); // Presets @@ -98,7 +85,7 @@ function initTinyMceProfileAssistant() { const toolbarSuggestions = toolbarButtons.slice(); toolbarSuggestions.push('|'); - let toolbarHtml = '' + (i18n.toolbar || 'Toolbar') + '

' + (i18n.toolbar_help || 'Manage one or more toolbars. Each row becomes one TinyMCE toolbar line.') + '

'; + let toolbarHtml = ' ' + (i18n.toolbar || 'Toolbar') + '

' + (i18n.toolbar_help || 'Manage one or more toolbars. Each row becomes one TinyMCE toolbar line.') + '

'; toolbarHtml += '

' + (i18n.toolbar_enabled_help || 'Disable this to generate toolbar: false. This keeps profiles without a toolbar compatible with TinyMCE.') + '

'; toolbarHtml += '
'; toolbarHtml += '
'; toolbarHtml += '
'; - // Common Settings - let settingsHtml = '
' + (i18n.common_settings || 'Common Settings') + '
'; - settingsHtml += '

' + (i18n.height_help || 'Zahl = Pixel, sonst gültiger CSS-Wert (px, pt, em). Laut TinyMCE-Doku sind %, vh und auto nicht unterstützt – für variable Höhe bitte Autoresize anhaken.') + '

'; - settingsHtml += '

' + (i18n.width_help || 'Optional. Zahl = Pixel, sonst CSS-Wert (unterstützt %, em, vh …). Leer lassen = volle Container-Breite.') + '

'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - - // Resize & Autoresize - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '

' + (i18n.autoresize_help || 'Aktiviert das autoresize-Plugin. min_height/max_height setzen die Grenzen des automatischen Wachstums, height wird ignoriert.') + '

'; - settingsHtml += '
'; - - // Advanced Settings - settingsHtml += '
' + (i18n.advanced_settings || 'Advanced Settings') + '
'; - - // Context Toolbar - settingsHtml += '

' + (i18n.context_toolbar_help || 'Shows tools directly at the cursor when text is selected.') + '

'; - settingsHtml += ''; - - // Auto Hide Toolbar - settingsHtml += '
'; - - // Image Options - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - - // URL Options - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - - settingsHtml += '
'; - - settingsHtml += '
'; - settingsHtml += '
'; - - settingsHtml += '
'; - - // Content Languages (für Sprach-Menü / language-Toolbar-Button) - settingsHtml += '
' + (i18n.content_langs || 'Sprach-Menü (content_langs)') + ''; - settingsHtml += '

' + (i18n.content_langs_help || 'Liste der Sprachen für den language-Toolbar-Button bzw. das Format-Menü. Markiert Textabschnitte mit einem lang-Attribut (wichtig für Screenreader/SEO).') + '

'; - settingsHtml += '
'; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += '
' + (i18n.content_langs_title || 'Titel') + '' + (i18n.content_langs_code || 'Code (lang)') + '' + (i18n.content_langs_customcode || 'Custom-Code (optional)') + '' + (i18n.content_langs_default || 'Standard') + '
'; - settingsHtml += ' '; - settingsHtml += ''; - settingsHtml += '

' + (i18n.content_langs_hint || 'Titel: Beschriftung im Menü. Code: BCP-47-Sprachcode (z. B. de, en, de-CH). Custom-Code: optional, wird als data-mce-lang gesetzt. Standard: setzt zusätzlich language (UI-Sprache) auf diesen Code. Leer lassen = Sprach-Menü deaktiviert.') + '

'; - settingsHtml += '
'; - - // Typografie-Autoreplace (for_chars_symbols) - settingsHtml += '
' + (i18n.autoreplace || 'Typografie-Autoreplace (for_chars_symbols)') + ''; - settingsHtml += '

' + (i18n.autoreplace_help || 'Live-Ersetzung beim Tippen. Ausgelöst durch Leer, Enter und Satzzeichen (. , ; : ! ? ) ] " \' /). Greift nicht in <code>, <pre>, <kbd>, <samp>, <tt>. Alle Ersetzungen sind Undo-fähig.') + '

'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += ''; - settingsHtml += '
' + (i18n.autoreplace_type || 'Typ') + '' + (i18n.autoreplace_from || 'Von (Text oder Regex)') + '' + (i18n.autoreplace_to || 'Nach (Ziel-Zeichen)') + '
'; - settingsHtml += ' '; - settingsHtml += ''; - settingsHtml += '

' + (i18n.autoreplace_hint || 'Typ: Text = wörtliche Ersetzung, Regex = regulärer Ausdruck mit Backreferences ($1, $2). Nach: Unicode-Escapes wie \\u00A0 (nbsp) oder \\u2011 (geschützter Bindestrich) sind erlaubt. Beispiele: Text (tel)+49 …; Regex \\(kw(\\d{1,2})\\)KW $1.') + '

'; - settingsHtml += '
'; - - // Extras (Codesample, RelList, TOC) - settingsHtml += '
' + (i18n.extras_defaults || 'Extras (Defaults)') + '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - - // Link-Defaults (target_list, noreferrer, https) - settingsHtml += '
' + (i18n.link_defaults || 'Link-Defaults') + '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - - // Color Mapping - settingsHtml += '
' + (i18n.color_mapping || 'Farb-Mapping (color_map_raw)') + ''; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += ''; - settingsHtml += '
' + (i18n.color_mapping_color || 'Farbe') + '' + (i18n.color_mapping_label || 'Label') + '
'; - settingsHtml += '
'; - settingsHtml += '

' + (i18n.color_map_raw_help || 'Gerade Anzahl an Einträgen: immer Farbe, dann Label. Beispiel: [\"#1d4ed8\",\"Blau\",\"#0f766e\",\"Türkis\"].') + '

'; - settingsHtml += '
'; - - // YForm Link Configuration - settingsHtml += '
Link YForm Configuration'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
TitleTableFieldLink-Schema (opt.)
'; - settingsHtml += '

' + (i18n.link_schema_help || 'Link-Schema (optional): URL-Muster für den erzeugten Link. Platzhalter: [id] = ID des YForm-Datensatzes, [field] = gewählter Feldwert. Beispiele: index.php?article_id=5&news=[id] oder /produkt/[id]. Leer lassen = es wird nur der Feldwert als Link-Text eingefügt.') + '

'; - settingsHtml += ''; - settingsHtml += '
'; - - // Image Width Plugin (Preset-based) - settingsHtml += '
' + (i18n.imagewidth || 'Bildformatierung') + ''; - settingsHtml += '

' + (i18n.imagewidth_help || 'Bilder werden in <figure> gewrappt. Breite, Ausrichtung und Effekte als CSS-Klassen.') + '

'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - // Preset Textareas - settingsHtml += ''; + // Heading-Helper fuer die Gruppen-Ueberschriften (Schritt 1..5). + // Visuelles Styling kommt aus css/profile_builder.css (Balken + Darkmode). + const groupTitle = (num, icon, label) => + '

' + + '' + num + '' + + '' + + '' + label + '' + + '

'; + + // ===== GRUPPE 1: GRUNDEINSTELLUNGEN ===== + // Editor-Geometrie, UI-Verhalten, URL-/Encoding-Optionen – alles "global wirkende". + let group1Html = groupTitle('1', 'fa-cog', i18n.group_basics || 'Grundeinstellungen'); + + // 1a) Editor-Geometrie & Sprache + group1Html += '' + (i18n.common_settings || 'Common Settings') + '
'; + group1Html += '

' + (i18n.height_help || 'Zahl = Pixel, sonst gültiger CSS-Wert (px, pt, em). Laut TinyMCE-Doku sind %, vh und auto nicht unterstützt – für variable Höhe bitte Autoresize anhaken.') + '

'; + group1Html += '

' + (i18n.width_help || 'Optional. Zahl = Pixel, sonst CSS-Wert (unterstützt %, em, vh …). Leer lassen = volle Container-Breite.') + '

'; + group1Html += '
'; + group1Html += '
'; + + // 1b) Resize & Autoresize + group1Html += '
'; + group1Html += '
'; + group1Html += '
'; + group1Html += '
'; + group1Html += '

' + (i18n.autoresize_help || 'Aktiviert das autoresize-Plugin. min_height/max_height setzen die Grenzen des automatischen Wachstums, height wird ignoriert.') + '

'; + group1Html += '
'; + + // 1c) Editor-Chrome: Menueleiste & Auto-Hide Toolbar + group1Html += '
'; + group1Html += '
'; + group1Html += '

' + (i18n.auto_hide_toolbar_help || 'Menue-, Werkzeug- und Statusleiste werden eingeklappt und erst beim Hovern oder wenn der Editor den Fokus bekommt eingeblendet. Spart vertikalen Platz in kompakten Formularen.') + '

'; + group1Html += '
'; + + // 1d) URL- und Encoding-Optionen (frueher quer ueber "Advanced" verteilt) + group1Html += '
' + (i18n.url_encoding_options || 'URL & Encoding') + '
'; + group1Html += '
'; + group1Html += '
'; + group1Html += '
'; + group1Html += '
'; + group1Html += '
'; + group1Html += '
'; + group1Html += '
'; + + // ===== GRUPPE 3: TOOLBARS & MENUES (Erweiterungen zur Haupt-Toolbar) ===== + // Haupt-Toolbar steckt bereits in toolbarHtml (oberhalb). Hier nur Context-Toolbar + // und Insert-Menue, beide jetzt gleichrangig nebeneinander – das Insert-Menue ist + // nicht mehr als Sub-Block der Context-Toolbar verschachtelt. + // Untergruppe von Schritt 3 – kein eigener Schritt-Header, nur ein Sub-Legend. + let group3ExtrasHtml = ' ' + (i18n.group_toolbar_extras || 'Context-Toolbar & Einfügen-Menü') + ''; - // Layout Rules - settingsHtml += '
' + (i18n.layout_rules || 'Layout-Regeln (Strukturoptimierung)') + ''; - settingsHtml += '

' + (i18n.layout_rules_help || 'Automatische Korrektur von häufigen Content-Struktur-Problemen. Stilschweigend und nicht-invasiv.') + '

'; - settingsHtml += '
'; - settingsHtml += '

' + (i18n.layout_no_images_in_headings_help || 'Bilder, die in h1-h6 sind, werden davor platziert.') + '

'; - settingsHtml += '

' + (i18n.layout_collapse_empty_help || 'Mehrere hintereinander folgende leere <p> werden durch ein Clear-Element ersetzt.') + '

'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '

' + (i18n.layout_delete_empty_help || 'Entfernt einzelne leere <p> am Anfang und Ende.') + '

'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - - // TOC Settings - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - settingsHtml += '
'; - - settingsHtml += '
' + (i18n.protected_extras || 'Protected extras') + ''; - settingsHtml += '

' + (i18n.protected_extras_help || 'Raw option lines entered here are appended after the generated options. Use this to keep custom TinyMCE settings or to intentionally override assistant-managed values.') + '

'; - settingsHtml += ''; + // Context Toolbar + group3ExtrasHtml += '
'; + group3ExtrasHtml += '

' + (i18n.context_toolbar_help || 'Shows tools directly at the cursor when text is selected.') + '

'; + group3ExtrasHtml += ''; + group3ExtrasHtml += '
'; + + // Insert Menue – eigenstaendig, nicht mehr in Context-Toolbar verschachtelt + group3ExtrasHtml += '
'; + group3ExtrasHtml += '
'; + group3ExtrasHtml += ''; + group3ExtrasHtml += '
'; + + // ===== GRUPPE 4: PRESETS & DEFAULTS ===== + // Sammelt alles, was Inhalte/Formate vor-konfiguriert (Sprachmenue, Farben, + // Link-Defaults, Codesample/RelList, YForm-Link). + let group4Html = groupTitle('4', 'fa-magic', i18n.group_presets || 'Presets & Defaults'); + + // 4a) Content Languages + group4Html += ' ' + (i18n.content_langs || 'Sprach-Menü (content_langs)') + ''; + group4Html += '

' + (i18n.content_langs_help || 'Liste der Sprachen für den language-Toolbar-Button bzw. das Format-Menü. Markiert Textabschnitte mit einem lang-Attribut (wichtig für Screenreader/SEO).') + '

'; + group4Html += '
'; + group4Html += ''; + group4Html += ''; + group4Html += ''; + group4Html += ''; + group4Html += ''; + group4Html += ''; + group4Html += '
' + (i18n.content_langs_title || 'Titel') + '' + (i18n.content_langs_code || 'Code (lang)') + '' + (i18n.content_langs_customcode || 'Custom-Code (optional)') + '' + (i18n.content_langs_default || 'Standard') + '
'; + group4Html += ' '; + group4Html += ''; + group4Html += '

' + (i18n.content_langs_hint || 'Titel: Beschriftung im Menü. Code: BCP-47-Sprachcode (z. B. de, en, de-CH). Custom-Code: optional, wird als data-mce-lang gesetzt. Standard: setzt zusätzlich language (UI-Sprache) auf diesen Code. Leer lassen = Sprach-Menü deaktiviert.') + '

'; + group4Html += '
'; + + // 4b) Color Mapping + group4Html += '
' + (i18n.color_mapping || 'Farb-Mapping (color_map_raw)') + ''; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += ''; + group4Html += '
' + (i18n.color_mapping_color || 'Farbe') + '' + (i18n.color_mapping_label || 'Label') + '
'; + group4Html += '
'; + group4Html += '

' + (i18n.color_map_raw_help || 'Gerade Anzahl an Einträgen: immer Farbe, dann Label. Beispiel: [\"#1d4ed8\",\"Blau\",\"#0f766e\",\"Türkis\"].') + '

'; + group4Html += '
'; + + // 4c) Link-Defaults + group4Html += '
' + (i18n.link_defaults || 'Link-Defaults') + '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + + // 4d) Extras (Codesample, RelList) + group4Html += '
' + (i18n.extras_defaults || 'Extras (Defaults)') + '
'; + group4Html += '
'; + group4Html += '
'; + group4Html += '
'; + + // 4e) YForm Link + group4Html += '
' + (i18n.link_yform_section || 'Link YForm Configuration') + ''; + group4Html += '
'; + group4Html += '
'; + group4Html += '
TitleTableFieldLink-Schema (opt.)
'; + group4Html += '

' + (i18n.link_schema_help || 'Link-Schema (optional): URL-Muster für den erzeugten Link. Platzhalter: [id] = ID des YForm-Datensatzes, [field] = gewählter Feldwert. Beispiele: index.php?article_id=5&news=[id] oder /produkt/[id]. Leer lassen = es wird nur der Feldwert als Link-Text eingefügt.') + '

'; + group4Html += ''; + group4Html += '
'; + + // ===== GRUPPE 5: ERWEITERTE OPTIONEN ===== + // Plugin-spezifische Konfiguration: Typografie-Autoreplace, FOR Images (inkl. + // frueher verstreuten Bild-Optionen), Layout-Regeln, TOC, Protected Extras. + let group5Html = groupTitle('5', 'fa-wrench', i18n.group_advanced || 'Erweiterte Optionen'); + + // 5a) Typografie-Autoreplace (for_chars_symbols) + group5Html += ' ' + (i18n.autoreplace || 'Typografie-Autoreplace (for_chars_symbols)') + ''; + group5Html += '

' + (i18n.autoreplace_help || 'Live-Ersetzung beim Tippen. Ausgelöst durch Leer, Enter und Satzzeichen (. , ; : ! ? ) ] " \' /). Greift nicht in <code>, <pre>, <kbd>, <samp>, <tt>. Alle Ersetzungen sind Undo-fähig.') + '

'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += ''; + group5Html += ''; + group5Html += ''; + group5Html += ''; + group5Html += ''; + group5Html += ''; + group5Html += '
' + (i18n.autoreplace_type || 'Typ') + '' + (i18n.autoreplace_from || 'Von (Text oder Regex)') + '' + (i18n.autoreplace_to || 'Nach (Ziel-Zeichen)') + '
'; + group5Html += ' '; + group5Html += ''; + group5Html += '

' + (i18n.autoreplace_hint || 'Typ: Text = wörtliche Ersetzung, Regex = regulärer Ausdruck mit Backreferences ($1, $2). Nach: Unicode-Escapes wie \\u00A0 (nbsp) oder \\u2011 (geschützter Bindestrich) sind erlaubt. Beispiele: Text (tel)+49 …; Regex \\(kw(\\d{1,2})\\)KW $1.') + '

'; + group5Html += '
'; + + // 5b) FOR Images – inkl. der frueher in "Advanced" verstreuten Bild-Optionen + group5Html += '
' + (i18n.imagewidth || 'FOR Images') + ' (for_images)'; + group5Html += '

' + (i18n.imagewidth_help || 'Erweiterte Bildformatierung über CSS-Klassen statt manuellem Resize: feste Breiten, Ausrichtung (links/rechts/zentriert), Effekt-Klassen (z. B. Schatten, Rahmen, Rundungen) und <figure>-Wrapping mit Caption. Presets für UIkit, Bootstrap und allgemeine Pixelwerte – inklusive Mediapool-Ersetzen direkt im Editor.') + '

'; + group5Html += '

' + (i18n.imagewidth_plugin_hint || 'Diese Option ist an das Plugin for_images gekoppelt: Wird for_images in Schritt 2 (Plugins) deaktiviert, wird dieser Block ebenfalls automatisch deaktiviert – und umgekehrt.') + '

'; + // 5b.1) Allgemeine Bild-Optionen (vorher in "Advanced" versteckt) + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + // 5b.2) FOR-Images Aktivierung + Template + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + // 5b.3) Preset Textareas + group5Html += ''; + + // 5c) Layout-Regeln + group5Html += '
' + (i18n.layout_rules || 'Layout-Regeln (Strukturoptimierung)') + ''; + group5Html += '

' + (i18n.layout_rules_help || 'Automatische Korrektur von häufigen Content-Struktur-Problemen. Stilschweigend und nicht-invasiv.') + '

'; + group5Html += '
'; + group5Html += '

' + (i18n.layout_no_images_in_headings_help || 'Bilder, die in h1-h6 sind, werden davor platziert.') + '

'; + group5Html += '

' + (i18n.layout_collapse_empty_help || 'Mehrere hintereinander folgende leere <p> werden durch ein Clear-Element ersetzt.') + '

'; + group5Html += '
'; + group5Html += '
'; + group5Html += '

' + (i18n.layout_delete_empty_help || 'Entfernt einzelne leere <p> am Anfang und Ende.') + '

'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + + // 5d) TOC – jetzt mit eigenem Legend, vorher als verwaiste Row an Layout-Regeln drangehaengt + group5Html += '
' + (i18n.toc_settings || 'Inhaltsverzeichnis (for_toc)') + ''; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + group5Html += '
'; + + // 5e) Protected Extras + group5Html += '
' + (i18n.protected_extras || 'Protected extras') + ''; + group5Html += '

' + (i18n.protected_extras_help || 'Raw option lines entered here are appended after the generated options. Use this to keep custom TinyMCE settings or to intentionally override assistant-managed values.') + '

'; + group5Html += ''; + + // Backwards-compatible alias: einige Stellen weiter unten erwarten settingsHtml. + // Wir setzen es auf die Plugin-/Defaults-Bloecke (Group 4 + 5); die Gruppen 1 und 3 + // werden separat im finalen $builderBody.html(...) eingefuegt. + let settingsHtml = group4Html + group5Html; // Apply Button(s): Generieren und Generieren+Speichern let actionsHtml = '
' @@ -336,7 +357,30 @@ function initTinyMceProfileAssistant() { + ' ' + '' + (i18n.overwrites_existing_config || 'Overwrites existing configuration!') + ''; - $builderBody.html(presetsHtml + pluginsHtml + toolbarHtml + settingsHtml + actionsHtml); + // Lineare 5-Schritte-Reihenfolge: + // Quick-Start-Presets -> 1. Grundeinstellungen -> 2. Plugins -> + // 3. Toolbar (Haupt) + 3b Context/Insert -> 4. Presets & Defaults -> + // 5. Erweiterte Optionen -> Apply-Buttons + // settingsHtml entspricht inhaltlich (group4Html + group5Html) und bleibt als + // Backwards-compat-Alias bestehen. + // Schritt-2-Header steht vor der Plugin-Liste (pluginsHtml beginnt mit einem + // eigenen Plugins als Untertitel, ohne Schritt-Nummer). + const group2HeaderHtml = groupTitle('2', 'fa-puzzle-piece', i18n.group_plugins || 'Plugins'); + // Schritt-3-Header steht vor der Haupt-Toolbar (toolbarHtml enthaelt nur das + // eigene Toolbar als Untertitel) und gruppiert Haupt-Toolbar + // + Context-Toolbar/Insert-Menue zu einem visuellen Block. + const group3HeaderHtml = groupTitle('3', 'fa-th-list', i18n.group_toolbars || 'Toolbars & Menüs'); + $builderBody.html( + presetsHtml + + group1Html + + group2HeaderHtml + + pluginsHtml + + group3HeaderHtml + + toolbarHtml + + group3ExtrasHtml + + settingsHtml + + actionsHtml, + ); // Context Toolbar Toggle $builderBody.on('change', '.builder-context-toolbar', function() { @@ -1944,6 +1988,31 @@ function initTinyMceProfileAssistant() { $builderBody.find('.builder-context-toolbar').prop('checked', $(this).is(':checked')).trigger('change'); }); + // Issue: for_images aus der Pluginliste entfernen -> auch im Assistenten-Block + // "Bildbreiten / for_images" deaktivieren (sonst bliebe das Plugin via + // imagewidthEnabled in der serialize-Stufe wieder in der Pluginliste). + $builderBody.on('change', '.builder-plugin[value="for_images"]', function() { + $builderBody.find('.builder-imagewidth-enable').prop('checked', $(this).is(':checked')).trigger('change'); + }); + + // Bi-direktionale Kopplung zwischen Autoresize-Checkbox (Schritt 1) und dem + // gleichnamigen Plugin in Schritt 2. Ohne diese Sync bliebe das Plugin in + // der Liste sichtbar abgewaehlt, obwohl die Option aktiv ist (und umgekehrt). + $builderBody.on('change', '.builder-autoresize', function() { + const enabled = $(this).is(':checked'); + const $plugin = $builderBody.find('.builder-plugin[value="autoresize"]'); + if ($plugin.length && $plugin.is(':checked') !== enabled) { + $plugin.prop('checked', enabled); + } + }); + $builderBody.on('change', '.builder-plugin[value="autoresize"]', function() { + const enabled = $(this).is(':checked'); + const $cb = $builderBody.find('.builder-autoresize'); + if ($cb.length && $cb.is(':checked') !== enabled) { + $cb.prop('checked', enabled); + } + }); + setToolbarRows([]); updateToolbarSettingsState(); @@ -3067,38 +3136,31 @@ function generateConfig($textarea, $builderBody) { configStr += 'setup: function (editor) {\n'; if (autoHideToolbar) { - configStr += ` editor.on('init', function() { + configStr += ` // Auto-Hide: Menue/Toolbar/Statusleiste nur sichtbar, solange der Editor + // den Fokus hat. Wird per CSS-Klasse + :focus-within umgesetzt – robuster + // als display:none-Inline, weil TinyMCE die UI mehrfach neu rendert. + editor.on('PostRender', function () { var container = editor.getContainer(); - var header = container.querySelector('.tox-editor-header'); - var statusbar = container.querySelector('.tox-statusbar'); - - if (header) header.style.display = 'none'; - if (statusbar) statusbar.style.display = 'none'; - - var show = function() { - if (header) header.style.display = ''; - if (statusbar) statusbar.style.display = ''; - }; - - var hide = function() { - if (header) header.style.display = 'none'; - if (statusbar) statusbar.style.display = 'none'; - }; - - editor.on('focus', show); - container.addEventListener('focusin', show); - - var hideTimeout; - var scheduleHide = function() { - hideTimeout = setTimeout(function() { - if (!container.contains(document.activeElement) && !editor.hasFocus()) { - hide(); - } - }, 100); - }; - - editor.on('blur', scheduleHide); - container.addEventListener('focusout', scheduleHide); + if (!container) { return; } + container.classList.add('tmce-autohide'); + if (document.getElementById('tmce-autohide-style')) { return; } + var style = document.createElement('style'); + style.id = 'tmce-autohide-style'; + style.textContent = '' + + '.tmce-autohide .tox-editor-header,' + + '.tmce-autohide .tox-statusbar { ' + + 'max-height: 0; overflow: hidden; opacity: 0; ' + + 'padding-top: 0 !important; padding-bottom: 0 !important; ' + + 'border: 0 !important; ' + + 'transition: max-height .15s ease, opacity .15s ease; }' + + '.tmce-autohide:focus-within .tox-editor-header,' + + '.tmce-autohide:hover .tox-editor-header,' + + '.tmce-autohide:focus-within .tox-statusbar,' + + '.tmce-autohide:hover .tox-statusbar { ' + + 'max-height: 400px; opacity: 1; ' + + 'padding-top: revert !important; padding-bottom: revert !important; ' + + 'border: revert !important; }'; + document.head.appendChild(style); });\n`; } configStr += '},\n'; @@ -3285,6 +3347,14 @@ function loadFromConfig($textarea, $builderBody) { $builderBody.find('.builder-menubar').prop('checked', !!cfg.menubar); } + // Auto-Hide Toolbar wird ueber einen setup()-Block serialisiert (siehe + // generateConfig). Da das in cfg nicht als eigenes Feld landet, erkennen + // wir den Zustand am Roh-Source des Profils anhand der CSS-Marker-Klasse + // bzw. (Legacy) am display:none-Inline-Toggle frueherer Versionen. + if (/tmce-autohide/.test(source) || /tox-editor-header[^]*display\s*=\s*['"]none['"]/.test(source)) { + $builderBody.find('.builder-auto-hide-toolbar').prop('checked', true); + } + // Context toolbar (quickbars) if (pluginList.indexOf('quickbars') !== -1 || typeof cfg.quickbars_selection_toolbar === 'string' || typeof cfg.quickbars_insert_toolbar !== 'undefined') { $builderBody.find('.builder-context-toolbar').prop('checked', true).trigger('change'); diff --git a/assets/scripts/tinymce/plugins/for_images/plugin.js b/assets/scripts/tinymce/plugins/for_images/plugin.js index afe7259b..5cb14da4 100644 --- a/assets/scripts/tinymce/plugins/for_images/plugin.js +++ b/assets/scripts/tinymce/plugins/for_images/plugin.js @@ -1,4 +1,4 @@ -(()=>{var G=[{label:"Original",class:""},{label:"Klein (150px)",class:"img-width-small"},{label:"Mittel (300px)",class:"img-width-medium"},{label:"Gro\xDF (600px)",class:"img-width-large"},{label:"100%",class:"img-width-full"}],j=[{label:"Keine",class:""},{label:"Links",class:"img-align-left"},{label:"Rechts",class:"img-align-right"},{label:"Zentriert",class:"img-align-center"}],q=[{label:"Kein Effekt",class:""}],I=class{constructor(n){this.cache=null;this.editor=n}get(){if(this.cache)return this.cache;let n=g=>{var r,f,p;let c=this.editor.options.get(g);return(!c||Array.isArray(c)&&c.length===0)&&(c=(r=this.editor.settings)==null?void 0:r[g]),(!c||Array.isArray(c)&&c.length===0)&&(c=(p=(f=this.editor).getParam)==null?void 0:p.call(f,g,null)),c},l=(g,c)=>Array.isArray(g)&&g.length>0?g:c;return this.cache={widthPresets:l(n("imagewidth_presets"),G),alignPresets:l(n("imagealign_presets"),j),effectPresets:l(n("imageeffect_presets"),q)},this.cache}invalidate(){this.cache=null}getAllClasses(n){let l=[];return n.forEach(g=>{g.class&&g.class.split(/\s+/).forEach(c=>{c&&!l.includes(c)&&l.push(c)})}),l}};function b(e){if(e.nodeName==="FIGURE")return e;let n=e.parentElement;return n&&n.nodeName==="FIGURE"?n:null}function _(e,n){let l=b(n);if(l)return l;let c=e.getDoc().createElement("figure"),r=n.parentElement;return r&&r.nodeName==="P"?Array.from(r.childNodes).some(p=>p!==n&&(p.nodeType!==Node.TEXT_NODE||(p.textContent||"").trim()!==""))?(r.parentNode.insertBefore(c,r),c.appendChild(n)):(r.parentNode.insertBefore(c,r),c.appendChild(n),r.remove()):r&&(r.insertBefore(c,n),c.appendChild(n)),c}function h(e){let n=e.selection.getNode();if(n.nodeName==="IMG")return n;if(n.nodeName==="FIGURE")return n.querySelector("img");if(n.nodeName==="FIGCAPTION"){let l=n.parentElement;if((l==null?void 0:l.nodeName)==="FIGURE")return l.querySelector("img")}return null}function D(e,n){let l=n!=null?n:e.getBody();if(!l)return;Array.from(l.querySelectorAll("img")).filter(r=>{let f=r.parentElement;return!(f&&f.nodeName==="FIGURE")}).forEach(r=>{_(e,r)}),Array.from(l.querySelectorAll("figure")).forEach(r=>{if(!r.querySelector("img")){let f=r.children.length>0,p=(r.textContent||"").trim()!=="";if(f||p)return;r.remove()}})}function H(e,n,l){if(n.forEach(g=>e.classList.remove(g)),l){let g=[];e.classList.forEach(r=>{(r==="image"||r==="image_resized"||r.startsWith("image-style-")||r.startsWith("img-width-")||r.startsWith("img-height-"))&&g.push(r)}),g.forEach(r=>e.classList.remove(r));let c=e.style.width;c&&c.includes("%")&&(e.style.removeProperty("width"),e.getAttribute("style")===""&&e.removeAttribute("style"))}}function L(e,n){A(n).forEach(g=>e.classList.remove(g))}function A(e){let n=[];return e.forEach(l=>{l.class&&l.class.split(/\s+/).forEach(g=>{g&&!n.includes(g)&&n.push(g)})}),n}function z(e){let n=A(e).join(" ");return n.includes("uk-width")?"uk-width-1-1":/\bw-\d/.test(n)?"w-100":""}function R(e,n){e.classList.remove("uk-width-1-1","w-100"),e.style.removeProperty("width"),n?e.classList.add(n):e.style.width="100%"}function U(e,n,l,g,c=!1){let r=l.class?_(e,n):b(n),f=z(g),p=A(g);r&&(H(r,p,c),l.class?(l.class.split(/\s+/).forEach(d=>{d&&r.classList.add(d)}),R(n,f)):f&&(f.split(/\s+/).forEach(d=>{d&&n.classList.remove(d)}),n.classList.length===0&&n.removeAttribute("class")),r.classList.length===0&&r.removeAttribute("class")),n.style.removeProperty("width"),n.style.removeProperty("height"),n.getAttribute("style")===""&&n.removeAttribute("style"),e.nodeChanged(),e.undoManager.add()}function W(e,n,l,g,c=!1){if(l.class){let r=_(e,n),f=l.class.split(/\s+/).filter(d=>d);f.every(d=>r.classList.contains(d))?f.forEach(d=>r.classList.remove(d)):f.forEach(d=>r.classList.add(d)),c&&H(r,A(g),!0),r.classList.length===0&&r.removeAttribute("class")}else{let r=b(n);r&&(H(r,A(g),c),r.classList.length===0&&r.removeAttribute("class"))}e.nodeChanged(),e.undoManager.add()}function x(e,n,l,g){U(e,n,l,g,!!e.options.get("image_compat_warn"))}function K(e,n,l,g){W(e,n,l,g,!!e.options.get("image_compat_warn"))}function w(e,n){let g=b(e)||e;for(let c of n){if(!c.class)continue;let r=c.class.split(/\s+/).filter(f=>f);if(r.length>0&&r.every(f=>g.classList.contains(f)))return c}return null}function B(e,n){let g=b(e)||e,c=[];for(let r of n){if(!r.class)continue;let f=r.class.split(/\s+/).filter(p=>p);f.length>0&&f.every(p=>g.classList.contains(p))&&c.push(r)}return c}var X=` +(()=>{var q=[{label:"Original",class:""},{label:"Klein (150px)",class:"img-width-small"},{label:"Mittel (300px)",class:"img-width-medium"},{label:"Gro\xDF (600px)",class:"img-width-large"},{label:"100%",class:"img-width-full"}],U=[{label:"Keine",class:""},{label:"Links",class:"img-align-left"},{label:"Rechts",class:"img-align-right"},{label:"Zentriert",class:"img-align-center"}],W=[{label:"Kein Effekt",class:""}],B=class{constructor(n){this.cache=null;this.editor=n}get(){if(this.cache)return this.cache;let n=g=>{var r,f,m;let c=this.editor.options.get(g);return(!c||Array.isArray(c)&&c.length===0)&&(c=(r=this.editor.settings)==null?void 0:r[g]),(!c||Array.isArray(c)&&c.length===0)&&(c=(m=(f=this.editor).getParam)==null?void 0:m.call(f,g,null)),c},o=(g,c)=>Array.isArray(g)&&g.length>0?g:c;return this.cache={widthPresets:o(n("imagewidth_presets"),q),alignPresets:o(n("imagealign_presets"),U),effectPresets:o(n("imageeffect_presets"),W)},this.cache}invalidate(){this.cache=null}getAllClasses(n){let o=[];return n.forEach(g=>{g.class&&g.class.split(/\s+/).forEach(c=>{c&&!o.includes(c)&&o.push(c)})}),o}};function y(e){if(e.nodeName==="FIGURE")return e;let n=e.parentElement;return n&&n.nodeName==="FIGURE"?n:null}function M(e,n){let o=y(n);if(o)return o;let c=e.getDoc().createElement("figure"),r=n.parentElement;return r&&r.nodeName==="P"?Array.from(r.childNodes).some(m=>m!==n&&(m.nodeType!==Node.TEXT_NODE||(m.textContent||"").trim()!==""))?(r.parentNode.insertBefore(c,r),c.appendChild(n)):(r.parentNode.insertBefore(c,r),c.appendChild(n),r.remove()):r&&(r.insertBefore(c,n),c.appendChild(n)),c}function p(e){let n=e.selection.getNode();if(n.nodeName==="IMG")return n;if(n.nodeName==="FIGURE")return n.querySelector("img");if(n.nodeName==="FIGCAPTION"){let o=n.parentElement;if((o==null?void 0:o.nodeName)==="FIGURE")return o.querySelector("img")}return null}function z(e,n){let o=n!=null?n:e.getBody();o&&Array.from(o.querySelectorAll("img, figure")).forEach(g=>{if(g.nodeName==="IMG"){let m=g.parentElement;m&&m.nodeName==="FIGURE"||M(e,g);return}let c=g;if(c.querySelector("img"))return;let r=c.children.length>0,f=(c.textContent||"").trim()!=="";!r&&!f&&c.remove()})}function C(e,n,o){if(n.forEach(g=>e.classList.remove(g)),o){let g=[];e.classList.forEach(r=>{(r==="image"||r==="image_resized"||r.startsWith("image-style-")||r.startsWith("img-width-")||r.startsWith("img-height-"))&&g.push(r)}),g.forEach(r=>e.classList.remove(r));let c=e.style.width;c&&c.includes("%")&&(e.style.removeProperty("width"),e.getAttribute("style")===""&&e.removeAttribute("style"))}}function P(e){let n=[];return e.forEach(o=>{o.class&&o.class.split(/\s+/).forEach(g=>{g&&!n.includes(g)&&n.push(g)})}),n}function D(e){let n=P(e).join(" ");return n.includes("uk-width")?"uk-width-1-1":/\bw-\d/.test(n)?"w-100":""}function F(e,n){e.classList.remove("uk-width-1-1","w-100"),e.style.removeProperty("width"),n?e.classList.add(n):e.style.width="100%"}function j(e,n,o,g,c=!1){let r=o.class?M(e,n):y(n),f=D(g),m=P(g);r&&(C(r,m,c),o.class?(o.class.split(/\s+/).forEach(d=>{d&&r.classList.add(d)}),F(n,f)):f&&(f.split(/\s+/).forEach(d=>{d&&n.classList.remove(d)}),n.classList.length===0&&n.removeAttribute("class")),r.classList.length===0&&r.removeAttribute("class")),n.style.removeProperty("width"),n.style.removeProperty("height"),n.getAttribute("style")===""&&n.removeAttribute("style"),e.nodeChanged(),e.undoManager.add()}function K(e,n,o,g,c=!1){if(o.class){let r=M(e,n),f=o.class.split(/\s+/).filter(d=>d);f.every(d=>r.classList.contains(d))?f.forEach(d=>r.classList.remove(d)):f.forEach(d=>r.classList.add(d)),c&&C(r,P(g),!0),r.classList.length===0&&r.removeAttribute("class")}else{let r=y(n);r&&(C(r,P(g),c),r.classList.length===0&&r.removeAttribute("class"))}e.nodeChanged(),e.undoManager.add()}function b(e,n,o,g){j(e,n,o,g,!!e.options.get("image_compat_warn"))}function V(e,n,o,g){K(e,n,o,g,!!e.options.get("image_compat_warn"))}function w(e,n){let g=y(e)||e;for(let c of n){if(!c.class)continue;let r=c.class.split(/\s+/).filter(f=>f);if(r.length>0&&r.every(f=>g.classList.contains(f)))return c}return null}function T(e,n){let g=y(e)||e,c=[];for(let r of n){if(!r.class)continue;let f=r.class.split(/\s+/).filter(m=>m);f.length>0&&f.every(m=>g.classList.contains(m))&&c.push(r)}return c}var Z=` figure { margin: 0; padding: 0; @@ -144,4 +144,4 @@ figure[class]:hover { outline-color: rgba(0,120,215,0.8); } -`,V=(e,n)=>{e.options.register("imagewidth_presets",{processor:"object[]",default:[]}),e.options.register("imagealign_presets",{processor:"object[]",default:[]}),e.options.register("imageeffect_presets",{processor:"object[]",default:[]}),e.options.register("image_compat_warn",{processor:"boolean",default:!1}),e.options.register("image_caption",{processor:"boolean",default:!1});let l=new I(e),g=!!e.options.get("image_compat_warn"),c=!1,r=t=>t?!!(t.querySelector("figure.image, figure.image_resized")||t.querySelector('[class*="image-style-"]')):!1,f=()=>{if(c||!g)return;let t=e.getBody();if(r(t)){c=!0;try{e.notificationManager.open({type:"warning",text:"Hinweis: Dieser Inhalt enth\xE4lt Bildmarkup aus dem alten CKEditor 5 (z. B. figure.image, image-style-\u2026). Bitte die betroffenen Bilder mit der neuen Bildformatierungs-Toolbar (Breite, Ausrichtung, Effekte) erneut formatieren und den Beitrag speichern, damit das Markup einheitlich wird.",closeButton:!0,timeout:12e3})}catch(i){}}};e.ui.registry.addIcon("for_imagedialog",''),e.ui.registry.addIcon("for_imagealignleft_icon",''),e.ui.registry.addIcon("for_imagealigncenter_icon",''),e.ui.registry.addIcon("for_imagealignright_icon",''),e.ui.registry.addIcon("for_imagealignnone_icon",''),e.ui.registry.addIcon("for_imageswappool_icon",''),e.on("init",()=>{let t=e.getDoc();if(t){let a=t.createElement("style");a.setAttribute("data-for-images-plugin","1"),a.textContent=X,t.head.appendChild(a)}let i=l.get(),s=z(i.widthPresets);e.getBody().querySelectorAll("figure").forEach(a=>{let m=a,v=m.querySelector("img");v&&m.classList.length>0&&R(v,s)}),f()}),e.on("SetContent",()=>{D(e),f()}),e.on("PastePostProcess",t=>{t!=null&&t.node&&D(e,t.node)}),e.on("ObjectResizeStart",t=>{var i;((i=t.target)==null?void 0:i.nodeName)==="IMG"&&t.preventDefault()}),e.options.set("object_resizing",!1);let p=e.options.isRegistered;typeof p=="function"&&p.call(e.options,"quickbars_image_toolbar")&&e.options.set("quickbars_image_toolbar",!1),e.options.set("image_caption",!1);let d=()=>{let t=h(e);if(!t)return null;let i=b(t);return i&&i.nodeName==="FIGURE"?i:null},S=()=>{let t=d();return t?(e.selection.select(t,!0),e.nodeChanged(),!0):!1};e.on("copy",t=>{let i=d();!i||!t.clipboardData||(t.clipboardData.setData("text/html",i.outerHTML),t.clipboardData.setData("text/plain",i.textContent||""),t.preventDefault())}),e.on("cut",t=>{let i=d();!i||!t.clipboardData||(t.clipboardData.setData("text/html",i.outerHTML),t.clipboardData.setData("text/plain",i.textContent||""),t.preventDefault(),e.undoManager.transact(()=>{e.dom.remove(i)}),e.nodeChanged())}),e.on("keydown",t=>{let i=String(t.key||"").toLowerCase();if(i==="delete"||i==="backspace"){let s=d();if(s){t.preventDefault(),e.undoManager.transact(()=>{e.dom.remove(s)}),e.nodeChanged();return}}(t.ctrlKey||t.metaKey)&&(i==="c"||i==="x")&&S()}),e.on("BeforeExecCommand",t=>{let i=String(t.command||"").toLowerCase();if(i==="copy"||i==="cut"){S();return}if(i==="delete"||i==="forwarddelete"||i==="mceDelete"){let s=d();if(!s)return;t.preventDefault(),e.undoManager.transact(()=>{e.dom.remove(s)}),e.nodeChanged();return}}),e.ui.registry.addMenuButton("imagewidth",{icon:"resize",tooltip:"Bildbreite",fetch:t=>{let i=h(e);if(!i){t([]);return}let s=l.get(),o=w(i,s.widthPresets);t(s.widthPresets.map(a=>({type:"menuitem",text:a.label,icon:o&&o.class===a.class?"checkmark":void 0,onAction:()=>x(e,i,a,s.widthPresets)})))}}),e.ui.registry.addToggleButton("for_imagealignleft",{icon:"for_imagealignleft_icon",tooltip:"Bild links",onAction:()=>{let t=h(e);if(!t)return;let i=l.get(),s=i.alignPresets.find(a=>a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")),o=w(t,i.alignPresets);o&&s&&o.class===s.class?x(e,t,{label:"",class:""},i.alignPresets):s&&x(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=l.get(),a=w(s,o.alignPresets);t.setActive(!!a&&(a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealigncenter",{icon:"for_imagealigncenter_icon",tooltip:"Bild zentrieren",onAction:()=>{let t=h(e);if(!t)return;let i=l.get(),s=i.alignPresets.find(a=>a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")),o=w(t,i.alignPresets);o&&s&&o.class===s.class?x(e,t,{label:"",class:""},i.alignPresets):s&&x(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=l.get(),a=w(s,o.alignPresets);t.setActive(!!a&&(a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealignright",{icon:"for_imagealignright_icon",tooltip:"Bild rechts",onAction:()=>{let t=h(e);if(!t)return;let i=l.get(),s=i.alignPresets.find(a=>a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")),o=w(t,i.alignPresets);o&&s&&o.class===s.class?x(e,t,{label:"",class:""},i.alignPresets):s&&x(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=l.get(),a=w(s,o.alignPresets);t.setActive(!!a&&(a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("for_imagealignnone",{icon:"for_imagealignnone_icon",tooltip:"Ausrichtung entfernen",onAction:()=>{let t=h(e);if(!t)return;let i=l.get();x(e,t,{label:"",class:""},i.alignPresets)}}),e.ui.registry.addMenuButton("imageeffect",{icon:"color-picker",tooltip:"Bild-Effekte",fetch:t=>{let i=h(e);if(!i){t([]);return}let s=l.get(),o=B(i,s.effectPresets);t(s.effectPresets.map(a=>({type:"togglemenuitem",text:a.label,active:a.class===""?o.length===0:o.some(m=>m.class===a.class),onAction:()=>K(e,i,a,s.effectPresets)})))}}),e.ui.registry.addButton("imagewidthdialog",{icon:"for_imagedialog",tooltip:"Bildformatierung",onAction:()=>{let t=h(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let i=l.get(),s=w(t,i.widthPresets),o=w(t,i.alignPresets),a=B(t,i.effectPresets),m=i.widthPresets.map(u=>({text:u.label,value:u.class})),v=i.alignPresets.map(u=>({text:u.label,value:u.class})),E=i.effectPresets.filter(u=>u.class).map(u=>({type:"checkbox",name:"effect_"+u.class.replace(/\s+/g,"_"),label:u.label})),y={widthClass:(s==null?void 0:s.class)||"",alignClass:(o==null?void 0:o.class)||""};i.effectPresets.filter(u=>u.class).forEach(u=>{y["effect_"+u.class.replace(/\s+/g,"_")]=a.some(P=>P.class===u.class)});let C=[{type:"selectbox",name:"widthClass",label:"Breite",items:m},{type:"selectbox",name:"alignClass",label:"Ausrichtung",items:v}];E.length>0&&C.push({type:"bar",items:E}),e.windowManager.open({title:"Bildformatierung",size:"normal",body:{type:"panel",items:C},buttons:[{type:"cancel",name:"cancel",text:"Abbrechen"},{type:"submit",name:"save",text:"\xDCbernehmen",primary:!0}],initialData:y,onSubmit:u=>{let P=u.getData(),M=i.widthPresets.find(k=>k.class===P.widthClass)||{label:"",class:""};x(e,t,M,i.widthPresets);let O=i.alignPresets.find(k=>k.class===P.alignClass)||{label:"",class:""};x(e,t,O,i.alignPresets);let T=b(t);T&&(L(T,i.effectPresets),i.effectPresets.filter(k=>k.class).forEach(k=>{P["effect_"+k.class.replace(/\s+/g,"_")]&&k.class.split(/\s+/).forEach(N=>{N&&T.classList.add(N)})})),e.nodeChanged(),e.undoManager.add(),u.close()}})}}),e.ui.registry.addContextToolbar("for_imagestoolbar",{predicate:t=>t.nodeName==="IMG"||t.nodeName==="FIGURE"&&t.querySelector("img"),items:"imagewidth for_imagealignleft for_imagealigncenter for_imagealignright imageeffect for_imagealignnone | imagealt imagecaption imageshowpool imageswappool",position:"node",scope:"node"}),e.ui.registry.addToggleButton("imagealt",{icon:"accessibility-check",tooltip:"Alt-Text bearbeiten",onAction:()=>{let t=h(e);if(!t)return;let i=t.getAttribute("alt")||"";e.windowManager.open({title:"Alt-Text (Bildbeschreibung)",body:{type:"panel",items:[{type:"input",name:"alttext",label:"Beschreibung f\xFCr Screenreader",placeholder:'z.B. "Rotes Auto vor blauem Himmel"'},{type:"htmlpanel",html:'

Beschreiben Sie den Bildinhalt kurz und pr\xE4gnant f\xFCr Menschen die das Bild nicht sehen k\xF6nnen.

'}]},initialData:{alttext:i},buttons:[{type:"cancel",text:"Abbrechen"},{type:"submit",text:"Speichern",primary:!0}],onSubmit:s=>{let o=s.getData();t.setAttribute("alt",o.alttext),e.nodeChanged(),e.undoManager.add(),s.close()}})},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=s.getAttribute("alt");t.setActive(!!o&&o.trim()!=="")};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("imagecaption",{icon:"comment",tooltip:"Bildunterschrift",onAction:()=>{var s;let t=h(e);if(!t)return;let i=b(t);if(i){let o=i.querySelector("figcaption");if(o)(s=o.textContent)!=null&&s.trim()?e.selection.setCursorLocation(o,0):o.remove();else{let m=e.getDoc().createElement("figcaption");m.innerHTML=" ",i.appendChild(m),e.selection.setCursorLocation(m,0)}}else{let o=_(e,t),m=e.getDoc().createElement("figcaption");m.innerHTML=" ",o.appendChild(m),e.selection.setCursorLocation(m,0)}e.nodeChanged(),e.undoManager.add()},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=b(s),a=(o==null?void 0:o.querySelector("figcaption"))!==null;t.setActive(a)};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("imageshowpool",{icon:"gallery",tooltip:"Zeige Medium im Medienpool",onAction:()=>{var o;let t=h(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let i=t.getAttribute("src");if(!i){e.notificationManager.open({text:"Bildquelle nicht gefunden.",type:"warning",timeout:3e3});return}let s=((o=i.split("/").pop())==null?void 0:o.split("?")[0])||"";if(s)try{let a="index.php?page=mediapool/media&file_name="+encodeURIComponent(s);window.open(a,"mediapool","width=1000,height=700,resizable=yes,scrollbars=yes"),e.notificationManager.open({text:`Medienpool \xF6ffnet sich f\xFCr: ${s}`,type:"info",timeout:3e3})}catch(a){e.notificationManager.open({text:"Medienpool konnte nicht ge\xF6ffnet werden.",type:"error",timeout:3e3})}}}),e.ui.registry.addButton("imageswappool",{icon:"for_imageswappool_icon",tooltip:"Austauschen aus Medienpool",onAction:()=>{let t=h(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}if(typeof openREXMedia!="function"){e.notificationManager.open({text:"Medienpool ist nicht verf\xFCgbar.",type:"error",timeout:3e3});return}try{let i=openREXMedia("tinymce_medialink","&args[types]=jpg%2Cjpeg%2Cpng%2Cgif%2Cbmp%2Ctiff%2Csvg%2Cwebp");typeof $!="undefined"&&$&&typeof $.fn.on=="function"?$(i).on("rex:selectMedia",function(s,o){var P;s.preventDefault(),i.close();let a=((P=o.split(".").pop())==null?void 0:P.toLowerCase())||"",v=["jpg","jpeg","png","gif","webp"].indexOf(a)!==-1?"/media/tiny/"+o:"/media/"+o,E=l.get(),y=b(t);y&&(L(y,E.widthPresets),L(y,E.alignPresets),L(y,E.effectPresets),stripCke5LegacyClasses(y),y.classList.length===0&&y.removeAttribute("class")),t.removeAttribute("width"),t.removeAttribute("height"),t.style.removeProperty("width"),t.style.removeProperty("height"),t.getAttribute("style")===""&&t.removeAttribute("style"),t.setAttribute("src",v);let C=()=>{t.removeEventListener("load",C),t.removeEventListener("error",u),e.nodeChanged(),e.undoManager.add()},u=()=>{t.removeEventListener("load",C),t.removeEventListener("error",u),e.nodeChanged(),e.undoManager.add()};t.addEventListener("load",C),t.addEventListener("error",u),typeof $!="undefined"&&$.getJSON&&$.getJSON("index.php?rex-api-call=tinymce_media_meta&file="+encodeURIComponent(o)).done(function(M){M&&M.alt&&t.setAttribute("alt",M.alt)}).fail(function(){}),e.notificationManager.open({text:`Bild erfolgreich aktualisiert: ${o}`,type:"success",timeout:3e3})}):e.notificationManager.open({text:"jQuery ist nicht verf\xFCgbar. Bitte verwenden Sie den Standard-Medienpool.",type:"warning",timeout:5e3})}catch(i){e.notificationManager.open({text:"Medienpool konnte nicht ge\xF6ffnet werden.",type:"error",timeout:3e3})}}}),e.on("NodeChange",()=>{let t=h(e);if(!t)return;let i=l.get(),s=w(t,i.widthPresets),o=w(t,i.alignPresets),a=B(t,i.effectPresets),m=[];s&&s.class&&m.push(s.label),o&&o.class&&m.push(o.label),a.length>0&&m.push(a.map(E=>E.label).join(", "));let v=b(t)||t;m.length>0?v.setAttribute("title",m.join(" \xB7 ")):v.removeAttribute("title")})},F=()=>{tinymce.PluginManager.add("for_images",V)};F();})(); +`,X=(e,n)=>{e.options.register("imagewidth_presets",{processor:"object[]",default:[]}),e.options.register("imagealign_presets",{processor:"object[]",default:[]}),e.options.register("imageeffect_presets",{processor:"object[]",default:[]}),e.options.register("image_compat_warn",{processor:"boolean",default:!1}),e.options.register("image_caption",{processor:"boolean",default:!1});let o=new B(e),g=!!e.options.get("image_compat_warn"),c=!1,r=t=>t?!!(t.querySelector("figure.image, figure.image_resized")||t.querySelector('[class*="image-style-"]')):!1,f=()=>{if(c||!g)return;let t=e.getBody();if(r(t)){c=!0;try{e.notificationManager.open({type:"warning",text:"Hinweis: Dieser Inhalt enth\xE4lt Bildmarkup aus dem alten CKEditor 5 (z. B. figure.image, image-style-\u2026). Bitte die betroffenen Bilder mit der neuen Bildformatierungs-Toolbar (Breite, Ausrichtung, Effekte) erneut formatieren und den Beitrag speichern, damit das Markup einheitlich wird.",closeButton:!0,timeout:12e3})}catch(i){}}};e.ui.registry.addIcon("for_imagedialog",''),e.ui.registry.addIcon("for_imagealignleft_icon",''),e.ui.registry.addIcon("for_imagealigncenter_icon",''),e.ui.registry.addIcon("for_imagealignright_icon",''),e.ui.registry.addIcon("for_imagealignnone_icon",''),e.ui.registry.addIcon("for_imageswappool_icon",''),e.on("init",()=>{let t=e.getDoc();if(t){let a=t.createElement("style");a.setAttribute("data-for-images-plugin","1"),a.textContent=Z,t.head.appendChild(a)}let i=o.get(),s=D(i.widthPresets);e.getBody().querySelectorAll("figure").forEach(a=>{let u=a,v=u.querySelector("img");v&&u.classList.length>0&&F(v,s)}),f()}),e.on("SetContent",()=>{z(e),f()}),e.on("PastePostProcess",t=>{t!=null&&t.node&&z(e,t.node)}),e.on("ObjectResizeStart",t=>{var i;((i=t.target)==null?void 0:i.nodeName)==="IMG"&&t.preventDefault()}),e.options.set("object_resizing",!1);let m=e.options.isRegistered;typeof m=="function"&&m.call(e.options,"quickbars_image_toolbar")&&e.options.set("quickbars_image_toolbar",!1),e.options.set("image_caption",!1);let d=()=>{let t=p(e);if(!t)return null;let i=y(t);return i&&i.nodeName==="FIGURE"?i:null},L=()=>{let t=d();return t?(e.selection.select(t,!0),e.nodeChanged(),!0):!1},H=(t,i)=>{i.setData("text/html",t.outerHTML),i.setData("text/plain",t.textContent||"")},A=t=>{e.undoManager.transact(()=>{e.dom.remove(t)}),e.nodeChanged()};e.on("copy",t=>{let i=d();!i||!t.clipboardData||(H(i,t.clipboardData),t.preventDefault())}),e.on("cut",t=>{let i=d();!i||!t.clipboardData||(H(i,t.clipboardData),t.preventDefault(),A(i))}),e.on("keydown",t=>{let i=String(t.key||"").toLowerCase();if(i==="delete"||i==="backspace"){let s=d();if(s){t.preventDefault(),A(s);return}}(t.ctrlKey||t.metaKey)&&(i==="c"||i==="x")&&L()}),e.on("BeforeExecCommand",t=>{let i=String(t.command||"").toLowerCase();if(i==="copy"||i==="cut"){L();return}if(i==="delete"||i==="forwarddelete"||i==="mcedelete"){let s=d();if(!s)return;t.preventDefault(),A(s)}}),e.ui.registry.addMenuButton("imagewidth",{icon:"resize",tooltip:"Bildbreite",fetch:t=>{let i=p(e);if(!i){t([]);return}let s=o.get(),l=w(i,s.widthPresets);t(s.widthPresets.map(a=>({type:"menuitem",text:a.label,icon:l&&l.class===a.class?"checkmark":void 0,onAction:()=>b(e,i,a,s.widthPresets)})))}}),e.ui.registry.addToggleButton("for_imagealignleft",{icon:"for_imagealignleft_icon",tooltip:"Bild links",onAction:()=>{let t=p(e);if(!t)return;let i=o.get(),s=i.alignPresets.find(a=>a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")),l=w(t,i.alignPresets);l&&s&&l.class===s.class?b(e,t,{label:"",class:""},i.alignPresets):s&&b(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=o.get(),a=w(s,l.alignPresets);t.setActive(!!a&&(a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealigncenter",{icon:"for_imagealigncenter_icon",tooltip:"Bild zentrieren",onAction:()=>{let t=p(e);if(!t)return;let i=o.get(),s=i.alignPresets.find(a=>a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")),l=w(t,i.alignPresets);l&&s&&l.class===s.class?b(e,t,{label:"",class:""},i.alignPresets):s&&b(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=o.get(),a=w(s,l.alignPresets);t.setActive(!!a&&(a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealignright",{icon:"for_imagealignright_icon",tooltip:"Bild rechts",onAction:()=>{let t=p(e);if(!t)return;let i=o.get(),s=i.alignPresets.find(a=>a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")),l=w(t,i.alignPresets);l&&s&&l.class===s.class?b(e,t,{label:"",class:""},i.alignPresets):s&&b(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=o.get(),a=w(s,l.alignPresets);t.setActive(!!a&&(a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("for_imagealignnone",{icon:"for_imagealignnone_icon",tooltip:"Ausrichtung entfernen",onAction:()=>{let t=p(e);if(!t)return;let i=o.get();b(e,t,{label:"",class:""},i.alignPresets)}}),e.ui.registry.addMenuButton("imageeffect",{icon:"color-picker",tooltip:"Bild-Effekte",fetch:t=>{let i=p(e);if(!i){t([]);return}let s=o.get(),l=T(i,s.effectPresets);t(s.effectPresets.map(a=>({type:"togglemenuitem",text:a.label,active:a.class===""?l.length===0:l.some(u=>u.class===a.class),onAction:()=>V(e,i,a,s.effectPresets)})))}}),e.ui.registry.addButton("imagewidthdialog",{icon:"for_imagedialog",tooltip:"Bildformatierung",onAction:()=>{let t=p(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let i=o.get(),s=w(t,i.widthPresets),l=w(t,i.alignPresets),a=T(t,i.effectPresets),u=i.widthPresets.map(h=>({text:h.label,value:h.class})),v=i.alignPresets.map(h=>({text:h.label,value:h.class})),E=i.effectPresets.filter(h=>h.class).map(h=>({type:"checkbox",name:"effect_"+h.class.replace(/\s+/g,"_"),label:h.label})),I={widthClass:(s==null?void 0:s.class)||"",alignClass:(l==null?void 0:l.class)||""};i.effectPresets.filter(h=>h.class).forEach(h=>{I["effect_"+h.class.replace(/\s+/g,"_")]=a.some(k=>k.class===h.class)});let N=[{type:"selectbox",name:"widthClass",label:"Breite",items:u},{type:"selectbox",name:"alignClass",label:"Ausrichtung",items:v}];E.length>0&&N.push({type:"bar",items:E}),e.windowManager.open({title:"Bildformatierung",size:"normal",body:{type:"panel",items:N},buttons:[{type:"cancel",name:"cancel",text:"Abbrechen"},{type:"submit",name:"save",text:"\xDCbernehmen",primary:!0}],initialData:I,onSubmit:h=>{let k=h.getData(),G=i.widthPresets.find(x=>x.class===k.widthClass)||{label:"",class:""};b(e,t,G,i.widthPresets);let O=i.alignPresets.find(x=>x.class===k.alignClass)||{label:"",class:""};b(e,t,O,i.alignPresets);let _=y(t);_&&(C(_,P(i.effectPresets),!1),i.effectPresets.filter(x=>x.class).forEach(x=>{k["effect_"+x.class.replace(/\s+/g,"_")]&&x.class.split(/\s+/).forEach(S=>{S&&_.classList.add(S)})})),e.nodeChanged(),e.undoManager.add(),h.close()}})}}),e.ui.registry.addContextToolbar("for_imagestoolbar",{predicate:t=>t.nodeName==="IMG"||t.nodeName==="FIGURE"&&t.querySelector("img"),items:"imagewidth for_imagealignleft for_imagealigncenter for_imagealignright imageeffect for_imagealignnone | imagealt imagecaption imageshowpool imageswappool",position:"node",scope:"node"}),e.ui.registry.addToggleButton("imagealt",{icon:"accessibility-check",tooltip:"Alt-Text bearbeiten",onAction:()=>{let t=p(e);if(!t)return;let i=t.getAttribute("alt")||"";e.windowManager.open({title:"Alt-Text (Bildbeschreibung)",body:{type:"panel",items:[{type:"input",name:"alttext",label:"Beschreibung f\xFCr Screenreader",placeholder:'z.B. "Rotes Auto vor blauem Himmel"'},{type:"htmlpanel",html:'

Beschreiben Sie den Bildinhalt kurz und pr\xE4gnant f\xFCr Menschen die das Bild nicht sehen k\xF6nnen.

'}]},initialData:{alttext:i},buttons:[{type:"cancel",text:"Abbrechen"},{type:"submit",text:"Speichern",primary:!0}],onSubmit:s=>{let l=s.getData();t.setAttribute("alt",l.alttext),e.nodeChanged(),e.undoManager.add(),s.close()}})},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=s.getAttribute("alt");t.setActive(!!l&&l.trim()!=="")};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("imagecaption",{icon:"comment",tooltip:"Bildunterschrift",onAction:()=>{var s;let t=p(e);if(!t)return;let i=y(t);if(i){let l=i.querySelector("figcaption");if(l)(s=l.textContent)!=null&&s.trim()?e.selection.setCursorLocation(l,0):l.remove();else{let u=e.getDoc().createElement("figcaption");u.innerHTML=" ",i.appendChild(u),e.selection.setCursorLocation(u,0)}}else{let l=M(e,t),u=e.getDoc().createElement("figcaption");u.innerHTML=" ",l.appendChild(u),e.selection.setCursorLocation(u,0)}e.nodeChanged(),e.undoManager.add()},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=y(s),a=(l==null?void 0:l.querySelector("figcaption"))!==null;t.setActive(a)};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("imageshowpool",{icon:"gallery",tooltip:"Zeige Medium im Medienpool",onAction:()=>{var l,a;let t=p(e),i=((a=(l=t==null?void 0:t.getAttribute("src"))==null?void 0:l.split("/").pop())==null?void 0:a.split("?")[0])||"";if(!i){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let s="index.php?page=mediapool/media&file_name="+encodeURIComponent(i);window.open(s,"mediapool","width=1000,height=700,resizable=yes,scrollbars=yes")}}),e.ui.registry.addButton("imageswappool",{icon:"for_imageswappool_icon",tooltip:"Austauschen aus Medienpool",onAction:()=>{let t=p(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}try{e.selection.select(t)}catch(s){}if(!(e.plugins&&e.plugins.image||typeof e.queryCommandSupported=="function"&&e.queryCommandSupported("mceImage"))){e.notificationManager.open({text:'Bild-Dialog nicht verf\xFCgbar \u2013 bitte das Plugin "image" im Profil aktivieren.',type:"warning",timeout:5e3});return}e.execCommand("mceImage")}}),e.on("NodeChange",()=>{let t=p(e);if(!t)return;let i=o.get(),s=w(t,i.widthPresets),l=w(t,i.alignPresets),a=T(t,i.effectPresets),u=[];s&&s.class&&u.push(s.label),l&&l.class&&u.push(l.label),a.length>0&&u.push(a.map(E=>E.label).join(", "));let v=y(t)||t;u.length>0?v.setAttribute("title",u.join(" \xB7 ")):v.removeAttribute("title")})},R=()=>{tinymce.PluginManager.add("for_images",X)};R();})(); diff --git a/assets/scripts/tinymce/plugins/for_images/plugin.min.js b/assets/scripts/tinymce/plugins/for_images/plugin.min.js index afe7259b..5cb14da4 100644 --- a/assets/scripts/tinymce/plugins/for_images/plugin.min.js +++ b/assets/scripts/tinymce/plugins/for_images/plugin.min.js @@ -1,4 +1,4 @@ -(()=>{var G=[{label:"Original",class:""},{label:"Klein (150px)",class:"img-width-small"},{label:"Mittel (300px)",class:"img-width-medium"},{label:"Gro\xDF (600px)",class:"img-width-large"},{label:"100%",class:"img-width-full"}],j=[{label:"Keine",class:""},{label:"Links",class:"img-align-left"},{label:"Rechts",class:"img-align-right"},{label:"Zentriert",class:"img-align-center"}],q=[{label:"Kein Effekt",class:""}],I=class{constructor(n){this.cache=null;this.editor=n}get(){if(this.cache)return this.cache;let n=g=>{var r,f,p;let c=this.editor.options.get(g);return(!c||Array.isArray(c)&&c.length===0)&&(c=(r=this.editor.settings)==null?void 0:r[g]),(!c||Array.isArray(c)&&c.length===0)&&(c=(p=(f=this.editor).getParam)==null?void 0:p.call(f,g,null)),c},l=(g,c)=>Array.isArray(g)&&g.length>0?g:c;return this.cache={widthPresets:l(n("imagewidth_presets"),G),alignPresets:l(n("imagealign_presets"),j),effectPresets:l(n("imageeffect_presets"),q)},this.cache}invalidate(){this.cache=null}getAllClasses(n){let l=[];return n.forEach(g=>{g.class&&g.class.split(/\s+/).forEach(c=>{c&&!l.includes(c)&&l.push(c)})}),l}};function b(e){if(e.nodeName==="FIGURE")return e;let n=e.parentElement;return n&&n.nodeName==="FIGURE"?n:null}function _(e,n){let l=b(n);if(l)return l;let c=e.getDoc().createElement("figure"),r=n.parentElement;return r&&r.nodeName==="P"?Array.from(r.childNodes).some(p=>p!==n&&(p.nodeType!==Node.TEXT_NODE||(p.textContent||"").trim()!==""))?(r.parentNode.insertBefore(c,r),c.appendChild(n)):(r.parentNode.insertBefore(c,r),c.appendChild(n),r.remove()):r&&(r.insertBefore(c,n),c.appendChild(n)),c}function h(e){let n=e.selection.getNode();if(n.nodeName==="IMG")return n;if(n.nodeName==="FIGURE")return n.querySelector("img");if(n.nodeName==="FIGCAPTION"){let l=n.parentElement;if((l==null?void 0:l.nodeName)==="FIGURE")return l.querySelector("img")}return null}function D(e,n){let l=n!=null?n:e.getBody();if(!l)return;Array.from(l.querySelectorAll("img")).filter(r=>{let f=r.parentElement;return!(f&&f.nodeName==="FIGURE")}).forEach(r=>{_(e,r)}),Array.from(l.querySelectorAll("figure")).forEach(r=>{if(!r.querySelector("img")){let f=r.children.length>0,p=(r.textContent||"").trim()!=="";if(f||p)return;r.remove()}})}function H(e,n,l){if(n.forEach(g=>e.classList.remove(g)),l){let g=[];e.classList.forEach(r=>{(r==="image"||r==="image_resized"||r.startsWith("image-style-")||r.startsWith("img-width-")||r.startsWith("img-height-"))&&g.push(r)}),g.forEach(r=>e.classList.remove(r));let c=e.style.width;c&&c.includes("%")&&(e.style.removeProperty("width"),e.getAttribute("style")===""&&e.removeAttribute("style"))}}function L(e,n){A(n).forEach(g=>e.classList.remove(g))}function A(e){let n=[];return e.forEach(l=>{l.class&&l.class.split(/\s+/).forEach(g=>{g&&!n.includes(g)&&n.push(g)})}),n}function z(e){let n=A(e).join(" ");return n.includes("uk-width")?"uk-width-1-1":/\bw-\d/.test(n)?"w-100":""}function R(e,n){e.classList.remove("uk-width-1-1","w-100"),e.style.removeProperty("width"),n?e.classList.add(n):e.style.width="100%"}function U(e,n,l,g,c=!1){let r=l.class?_(e,n):b(n),f=z(g),p=A(g);r&&(H(r,p,c),l.class?(l.class.split(/\s+/).forEach(d=>{d&&r.classList.add(d)}),R(n,f)):f&&(f.split(/\s+/).forEach(d=>{d&&n.classList.remove(d)}),n.classList.length===0&&n.removeAttribute("class")),r.classList.length===0&&r.removeAttribute("class")),n.style.removeProperty("width"),n.style.removeProperty("height"),n.getAttribute("style")===""&&n.removeAttribute("style"),e.nodeChanged(),e.undoManager.add()}function W(e,n,l,g,c=!1){if(l.class){let r=_(e,n),f=l.class.split(/\s+/).filter(d=>d);f.every(d=>r.classList.contains(d))?f.forEach(d=>r.classList.remove(d)):f.forEach(d=>r.classList.add(d)),c&&H(r,A(g),!0),r.classList.length===0&&r.removeAttribute("class")}else{let r=b(n);r&&(H(r,A(g),c),r.classList.length===0&&r.removeAttribute("class"))}e.nodeChanged(),e.undoManager.add()}function x(e,n,l,g){U(e,n,l,g,!!e.options.get("image_compat_warn"))}function K(e,n,l,g){W(e,n,l,g,!!e.options.get("image_compat_warn"))}function w(e,n){let g=b(e)||e;for(let c of n){if(!c.class)continue;let r=c.class.split(/\s+/).filter(f=>f);if(r.length>0&&r.every(f=>g.classList.contains(f)))return c}return null}function B(e,n){let g=b(e)||e,c=[];for(let r of n){if(!r.class)continue;let f=r.class.split(/\s+/).filter(p=>p);f.length>0&&f.every(p=>g.classList.contains(p))&&c.push(r)}return c}var X=` +(()=>{var q=[{label:"Original",class:""},{label:"Klein (150px)",class:"img-width-small"},{label:"Mittel (300px)",class:"img-width-medium"},{label:"Gro\xDF (600px)",class:"img-width-large"},{label:"100%",class:"img-width-full"}],U=[{label:"Keine",class:""},{label:"Links",class:"img-align-left"},{label:"Rechts",class:"img-align-right"},{label:"Zentriert",class:"img-align-center"}],W=[{label:"Kein Effekt",class:""}],B=class{constructor(n){this.cache=null;this.editor=n}get(){if(this.cache)return this.cache;let n=g=>{var r,f,m;let c=this.editor.options.get(g);return(!c||Array.isArray(c)&&c.length===0)&&(c=(r=this.editor.settings)==null?void 0:r[g]),(!c||Array.isArray(c)&&c.length===0)&&(c=(m=(f=this.editor).getParam)==null?void 0:m.call(f,g,null)),c},o=(g,c)=>Array.isArray(g)&&g.length>0?g:c;return this.cache={widthPresets:o(n("imagewidth_presets"),q),alignPresets:o(n("imagealign_presets"),U),effectPresets:o(n("imageeffect_presets"),W)},this.cache}invalidate(){this.cache=null}getAllClasses(n){let o=[];return n.forEach(g=>{g.class&&g.class.split(/\s+/).forEach(c=>{c&&!o.includes(c)&&o.push(c)})}),o}};function y(e){if(e.nodeName==="FIGURE")return e;let n=e.parentElement;return n&&n.nodeName==="FIGURE"?n:null}function M(e,n){let o=y(n);if(o)return o;let c=e.getDoc().createElement("figure"),r=n.parentElement;return r&&r.nodeName==="P"?Array.from(r.childNodes).some(m=>m!==n&&(m.nodeType!==Node.TEXT_NODE||(m.textContent||"").trim()!==""))?(r.parentNode.insertBefore(c,r),c.appendChild(n)):(r.parentNode.insertBefore(c,r),c.appendChild(n),r.remove()):r&&(r.insertBefore(c,n),c.appendChild(n)),c}function p(e){let n=e.selection.getNode();if(n.nodeName==="IMG")return n;if(n.nodeName==="FIGURE")return n.querySelector("img");if(n.nodeName==="FIGCAPTION"){let o=n.parentElement;if((o==null?void 0:o.nodeName)==="FIGURE")return o.querySelector("img")}return null}function z(e,n){let o=n!=null?n:e.getBody();o&&Array.from(o.querySelectorAll("img, figure")).forEach(g=>{if(g.nodeName==="IMG"){let m=g.parentElement;m&&m.nodeName==="FIGURE"||M(e,g);return}let c=g;if(c.querySelector("img"))return;let r=c.children.length>0,f=(c.textContent||"").trim()!=="";!r&&!f&&c.remove()})}function C(e,n,o){if(n.forEach(g=>e.classList.remove(g)),o){let g=[];e.classList.forEach(r=>{(r==="image"||r==="image_resized"||r.startsWith("image-style-")||r.startsWith("img-width-")||r.startsWith("img-height-"))&&g.push(r)}),g.forEach(r=>e.classList.remove(r));let c=e.style.width;c&&c.includes("%")&&(e.style.removeProperty("width"),e.getAttribute("style")===""&&e.removeAttribute("style"))}}function P(e){let n=[];return e.forEach(o=>{o.class&&o.class.split(/\s+/).forEach(g=>{g&&!n.includes(g)&&n.push(g)})}),n}function D(e){let n=P(e).join(" ");return n.includes("uk-width")?"uk-width-1-1":/\bw-\d/.test(n)?"w-100":""}function F(e,n){e.classList.remove("uk-width-1-1","w-100"),e.style.removeProperty("width"),n?e.classList.add(n):e.style.width="100%"}function j(e,n,o,g,c=!1){let r=o.class?M(e,n):y(n),f=D(g),m=P(g);r&&(C(r,m,c),o.class?(o.class.split(/\s+/).forEach(d=>{d&&r.classList.add(d)}),F(n,f)):f&&(f.split(/\s+/).forEach(d=>{d&&n.classList.remove(d)}),n.classList.length===0&&n.removeAttribute("class")),r.classList.length===0&&r.removeAttribute("class")),n.style.removeProperty("width"),n.style.removeProperty("height"),n.getAttribute("style")===""&&n.removeAttribute("style"),e.nodeChanged(),e.undoManager.add()}function K(e,n,o,g,c=!1){if(o.class){let r=M(e,n),f=o.class.split(/\s+/).filter(d=>d);f.every(d=>r.classList.contains(d))?f.forEach(d=>r.classList.remove(d)):f.forEach(d=>r.classList.add(d)),c&&C(r,P(g),!0),r.classList.length===0&&r.removeAttribute("class")}else{let r=y(n);r&&(C(r,P(g),c),r.classList.length===0&&r.removeAttribute("class"))}e.nodeChanged(),e.undoManager.add()}function b(e,n,o,g){j(e,n,o,g,!!e.options.get("image_compat_warn"))}function V(e,n,o,g){K(e,n,o,g,!!e.options.get("image_compat_warn"))}function w(e,n){let g=y(e)||e;for(let c of n){if(!c.class)continue;let r=c.class.split(/\s+/).filter(f=>f);if(r.length>0&&r.every(f=>g.classList.contains(f)))return c}return null}function T(e,n){let g=y(e)||e,c=[];for(let r of n){if(!r.class)continue;let f=r.class.split(/\s+/).filter(m=>m);f.length>0&&f.every(m=>g.classList.contains(m))&&c.push(r)}return c}var Z=` figure { margin: 0; padding: 0; @@ -144,4 +144,4 @@ figure[class]:hover { outline-color: rgba(0,120,215,0.8); } -`,V=(e,n)=>{e.options.register("imagewidth_presets",{processor:"object[]",default:[]}),e.options.register("imagealign_presets",{processor:"object[]",default:[]}),e.options.register("imageeffect_presets",{processor:"object[]",default:[]}),e.options.register("image_compat_warn",{processor:"boolean",default:!1}),e.options.register("image_caption",{processor:"boolean",default:!1});let l=new I(e),g=!!e.options.get("image_compat_warn"),c=!1,r=t=>t?!!(t.querySelector("figure.image, figure.image_resized")||t.querySelector('[class*="image-style-"]')):!1,f=()=>{if(c||!g)return;let t=e.getBody();if(r(t)){c=!0;try{e.notificationManager.open({type:"warning",text:"Hinweis: Dieser Inhalt enth\xE4lt Bildmarkup aus dem alten CKEditor 5 (z. B. figure.image, image-style-\u2026). Bitte die betroffenen Bilder mit der neuen Bildformatierungs-Toolbar (Breite, Ausrichtung, Effekte) erneut formatieren und den Beitrag speichern, damit das Markup einheitlich wird.",closeButton:!0,timeout:12e3})}catch(i){}}};e.ui.registry.addIcon("for_imagedialog",''),e.ui.registry.addIcon("for_imagealignleft_icon",''),e.ui.registry.addIcon("for_imagealigncenter_icon",''),e.ui.registry.addIcon("for_imagealignright_icon",''),e.ui.registry.addIcon("for_imagealignnone_icon",''),e.ui.registry.addIcon("for_imageswappool_icon",''),e.on("init",()=>{let t=e.getDoc();if(t){let a=t.createElement("style");a.setAttribute("data-for-images-plugin","1"),a.textContent=X,t.head.appendChild(a)}let i=l.get(),s=z(i.widthPresets);e.getBody().querySelectorAll("figure").forEach(a=>{let m=a,v=m.querySelector("img");v&&m.classList.length>0&&R(v,s)}),f()}),e.on("SetContent",()=>{D(e),f()}),e.on("PastePostProcess",t=>{t!=null&&t.node&&D(e,t.node)}),e.on("ObjectResizeStart",t=>{var i;((i=t.target)==null?void 0:i.nodeName)==="IMG"&&t.preventDefault()}),e.options.set("object_resizing",!1);let p=e.options.isRegistered;typeof p=="function"&&p.call(e.options,"quickbars_image_toolbar")&&e.options.set("quickbars_image_toolbar",!1),e.options.set("image_caption",!1);let d=()=>{let t=h(e);if(!t)return null;let i=b(t);return i&&i.nodeName==="FIGURE"?i:null},S=()=>{let t=d();return t?(e.selection.select(t,!0),e.nodeChanged(),!0):!1};e.on("copy",t=>{let i=d();!i||!t.clipboardData||(t.clipboardData.setData("text/html",i.outerHTML),t.clipboardData.setData("text/plain",i.textContent||""),t.preventDefault())}),e.on("cut",t=>{let i=d();!i||!t.clipboardData||(t.clipboardData.setData("text/html",i.outerHTML),t.clipboardData.setData("text/plain",i.textContent||""),t.preventDefault(),e.undoManager.transact(()=>{e.dom.remove(i)}),e.nodeChanged())}),e.on("keydown",t=>{let i=String(t.key||"").toLowerCase();if(i==="delete"||i==="backspace"){let s=d();if(s){t.preventDefault(),e.undoManager.transact(()=>{e.dom.remove(s)}),e.nodeChanged();return}}(t.ctrlKey||t.metaKey)&&(i==="c"||i==="x")&&S()}),e.on("BeforeExecCommand",t=>{let i=String(t.command||"").toLowerCase();if(i==="copy"||i==="cut"){S();return}if(i==="delete"||i==="forwarddelete"||i==="mceDelete"){let s=d();if(!s)return;t.preventDefault(),e.undoManager.transact(()=>{e.dom.remove(s)}),e.nodeChanged();return}}),e.ui.registry.addMenuButton("imagewidth",{icon:"resize",tooltip:"Bildbreite",fetch:t=>{let i=h(e);if(!i){t([]);return}let s=l.get(),o=w(i,s.widthPresets);t(s.widthPresets.map(a=>({type:"menuitem",text:a.label,icon:o&&o.class===a.class?"checkmark":void 0,onAction:()=>x(e,i,a,s.widthPresets)})))}}),e.ui.registry.addToggleButton("for_imagealignleft",{icon:"for_imagealignleft_icon",tooltip:"Bild links",onAction:()=>{let t=h(e);if(!t)return;let i=l.get(),s=i.alignPresets.find(a=>a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")),o=w(t,i.alignPresets);o&&s&&o.class===s.class?x(e,t,{label:"",class:""},i.alignPresets):s&&x(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=l.get(),a=w(s,o.alignPresets);t.setActive(!!a&&(a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealigncenter",{icon:"for_imagealigncenter_icon",tooltip:"Bild zentrieren",onAction:()=>{let t=h(e);if(!t)return;let i=l.get(),s=i.alignPresets.find(a=>a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")),o=w(t,i.alignPresets);o&&s&&o.class===s.class?x(e,t,{label:"",class:""},i.alignPresets):s&&x(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=l.get(),a=w(s,o.alignPresets);t.setActive(!!a&&(a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealignright",{icon:"for_imagealignright_icon",tooltip:"Bild rechts",onAction:()=>{let t=h(e);if(!t)return;let i=l.get(),s=i.alignPresets.find(a=>a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")),o=w(t,i.alignPresets);o&&s&&o.class===s.class?x(e,t,{label:"",class:""},i.alignPresets):s&&x(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=l.get(),a=w(s,o.alignPresets);t.setActive(!!a&&(a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("for_imagealignnone",{icon:"for_imagealignnone_icon",tooltip:"Ausrichtung entfernen",onAction:()=>{let t=h(e);if(!t)return;let i=l.get();x(e,t,{label:"",class:""},i.alignPresets)}}),e.ui.registry.addMenuButton("imageeffect",{icon:"color-picker",tooltip:"Bild-Effekte",fetch:t=>{let i=h(e);if(!i){t([]);return}let s=l.get(),o=B(i,s.effectPresets);t(s.effectPresets.map(a=>({type:"togglemenuitem",text:a.label,active:a.class===""?o.length===0:o.some(m=>m.class===a.class),onAction:()=>K(e,i,a,s.effectPresets)})))}}),e.ui.registry.addButton("imagewidthdialog",{icon:"for_imagedialog",tooltip:"Bildformatierung",onAction:()=>{let t=h(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let i=l.get(),s=w(t,i.widthPresets),o=w(t,i.alignPresets),a=B(t,i.effectPresets),m=i.widthPresets.map(u=>({text:u.label,value:u.class})),v=i.alignPresets.map(u=>({text:u.label,value:u.class})),E=i.effectPresets.filter(u=>u.class).map(u=>({type:"checkbox",name:"effect_"+u.class.replace(/\s+/g,"_"),label:u.label})),y={widthClass:(s==null?void 0:s.class)||"",alignClass:(o==null?void 0:o.class)||""};i.effectPresets.filter(u=>u.class).forEach(u=>{y["effect_"+u.class.replace(/\s+/g,"_")]=a.some(P=>P.class===u.class)});let C=[{type:"selectbox",name:"widthClass",label:"Breite",items:m},{type:"selectbox",name:"alignClass",label:"Ausrichtung",items:v}];E.length>0&&C.push({type:"bar",items:E}),e.windowManager.open({title:"Bildformatierung",size:"normal",body:{type:"panel",items:C},buttons:[{type:"cancel",name:"cancel",text:"Abbrechen"},{type:"submit",name:"save",text:"\xDCbernehmen",primary:!0}],initialData:y,onSubmit:u=>{let P=u.getData(),M=i.widthPresets.find(k=>k.class===P.widthClass)||{label:"",class:""};x(e,t,M,i.widthPresets);let O=i.alignPresets.find(k=>k.class===P.alignClass)||{label:"",class:""};x(e,t,O,i.alignPresets);let T=b(t);T&&(L(T,i.effectPresets),i.effectPresets.filter(k=>k.class).forEach(k=>{P["effect_"+k.class.replace(/\s+/g,"_")]&&k.class.split(/\s+/).forEach(N=>{N&&T.classList.add(N)})})),e.nodeChanged(),e.undoManager.add(),u.close()}})}}),e.ui.registry.addContextToolbar("for_imagestoolbar",{predicate:t=>t.nodeName==="IMG"||t.nodeName==="FIGURE"&&t.querySelector("img"),items:"imagewidth for_imagealignleft for_imagealigncenter for_imagealignright imageeffect for_imagealignnone | imagealt imagecaption imageshowpool imageswappool",position:"node",scope:"node"}),e.ui.registry.addToggleButton("imagealt",{icon:"accessibility-check",tooltip:"Alt-Text bearbeiten",onAction:()=>{let t=h(e);if(!t)return;let i=t.getAttribute("alt")||"";e.windowManager.open({title:"Alt-Text (Bildbeschreibung)",body:{type:"panel",items:[{type:"input",name:"alttext",label:"Beschreibung f\xFCr Screenreader",placeholder:'z.B. "Rotes Auto vor blauem Himmel"'},{type:"htmlpanel",html:'

Beschreiben Sie den Bildinhalt kurz und pr\xE4gnant f\xFCr Menschen die das Bild nicht sehen k\xF6nnen.

'}]},initialData:{alttext:i},buttons:[{type:"cancel",text:"Abbrechen"},{type:"submit",text:"Speichern",primary:!0}],onSubmit:s=>{let o=s.getData();t.setAttribute("alt",o.alttext),e.nodeChanged(),e.undoManager.add(),s.close()}})},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=s.getAttribute("alt");t.setActive(!!o&&o.trim()!=="")};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("imagecaption",{icon:"comment",tooltip:"Bildunterschrift",onAction:()=>{var s;let t=h(e);if(!t)return;let i=b(t);if(i){let o=i.querySelector("figcaption");if(o)(s=o.textContent)!=null&&s.trim()?e.selection.setCursorLocation(o,0):o.remove();else{let m=e.getDoc().createElement("figcaption");m.innerHTML=" ",i.appendChild(m),e.selection.setCursorLocation(m,0)}}else{let o=_(e,t),m=e.getDoc().createElement("figcaption");m.innerHTML=" ",o.appendChild(m),e.selection.setCursorLocation(m,0)}e.nodeChanged(),e.undoManager.add()},onSetup:t=>{let i=()=>{let s=h(e);if(!s){t.setActive(!1);return}let o=b(s),a=(o==null?void 0:o.querySelector("figcaption"))!==null;t.setActive(a)};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("imageshowpool",{icon:"gallery",tooltip:"Zeige Medium im Medienpool",onAction:()=>{var o;let t=h(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let i=t.getAttribute("src");if(!i){e.notificationManager.open({text:"Bildquelle nicht gefunden.",type:"warning",timeout:3e3});return}let s=((o=i.split("/").pop())==null?void 0:o.split("?")[0])||"";if(s)try{let a="index.php?page=mediapool/media&file_name="+encodeURIComponent(s);window.open(a,"mediapool","width=1000,height=700,resizable=yes,scrollbars=yes"),e.notificationManager.open({text:`Medienpool \xF6ffnet sich f\xFCr: ${s}`,type:"info",timeout:3e3})}catch(a){e.notificationManager.open({text:"Medienpool konnte nicht ge\xF6ffnet werden.",type:"error",timeout:3e3})}}}),e.ui.registry.addButton("imageswappool",{icon:"for_imageswappool_icon",tooltip:"Austauschen aus Medienpool",onAction:()=>{let t=h(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}if(typeof openREXMedia!="function"){e.notificationManager.open({text:"Medienpool ist nicht verf\xFCgbar.",type:"error",timeout:3e3});return}try{let i=openREXMedia("tinymce_medialink","&args[types]=jpg%2Cjpeg%2Cpng%2Cgif%2Cbmp%2Ctiff%2Csvg%2Cwebp");typeof $!="undefined"&&$&&typeof $.fn.on=="function"?$(i).on("rex:selectMedia",function(s,o){var P;s.preventDefault(),i.close();let a=((P=o.split(".").pop())==null?void 0:P.toLowerCase())||"",v=["jpg","jpeg","png","gif","webp"].indexOf(a)!==-1?"/media/tiny/"+o:"/media/"+o,E=l.get(),y=b(t);y&&(L(y,E.widthPresets),L(y,E.alignPresets),L(y,E.effectPresets),stripCke5LegacyClasses(y),y.classList.length===0&&y.removeAttribute("class")),t.removeAttribute("width"),t.removeAttribute("height"),t.style.removeProperty("width"),t.style.removeProperty("height"),t.getAttribute("style")===""&&t.removeAttribute("style"),t.setAttribute("src",v);let C=()=>{t.removeEventListener("load",C),t.removeEventListener("error",u),e.nodeChanged(),e.undoManager.add()},u=()=>{t.removeEventListener("load",C),t.removeEventListener("error",u),e.nodeChanged(),e.undoManager.add()};t.addEventListener("load",C),t.addEventListener("error",u),typeof $!="undefined"&&$.getJSON&&$.getJSON("index.php?rex-api-call=tinymce_media_meta&file="+encodeURIComponent(o)).done(function(M){M&&M.alt&&t.setAttribute("alt",M.alt)}).fail(function(){}),e.notificationManager.open({text:`Bild erfolgreich aktualisiert: ${o}`,type:"success",timeout:3e3})}):e.notificationManager.open({text:"jQuery ist nicht verf\xFCgbar. Bitte verwenden Sie den Standard-Medienpool.",type:"warning",timeout:5e3})}catch(i){e.notificationManager.open({text:"Medienpool konnte nicht ge\xF6ffnet werden.",type:"error",timeout:3e3})}}}),e.on("NodeChange",()=>{let t=h(e);if(!t)return;let i=l.get(),s=w(t,i.widthPresets),o=w(t,i.alignPresets),a=B(t,i.effectPresets),m=[];s&&s.class&&m.push(s.label),o&&o.class&&m.push(o.label),a.length>0&&m.push(a.map(E=>E.label).join(", "));let v=b(t)||t;m.length>0?v.setAttribute("title",m.join(" \xB7 ")):v.removeAttribute("title")})},F=()=>{tinymce.PluginManager.add("for_images",V)};F();})(); +`,X=(e,n)=>{e.options.register("imagewidth_presets",{processor:"object[]",default:[]}),e.options.register("imagealign_presets",{processor:"object[]",default:[]}),e.options.register("imageeffect_presets",{processor:"object[]",default:[]}),e.options.register("image_compat_warn",{processor:"boolean",default:!1}),e.options.register("image_caption",{processor:"boolean",default:!1});let o=new B(e),g=!!e.options.get("image_compat_warn"),c=!1,r=t=>t?!!(t.querySelector("figure.image, figure.image_resized")||t.querySelector('[class*="image-style-"]')):!1,f=()=>{if(c||!g)return;let t=e.getBody();if(r(t)){c=!0;try{e.notificationManager.open({type:"warning",text:"Hinweis: Dieser Inhalt enth\xE4lt Bildmarkup aus dem alten CKEditor 5 (z. B. figure.image, image-style-\u2026). Bitte die betroffenen Bilder mit der neuen Bildformatierungs-Toolbar (Breite, Ausrichtung, Effekte) erneut formatieren und den Beitrag speichern, damit das Markup einheitlich wird.",closeButton:!0,timeout:12e3})}catch(i){}}};e.ui.registry.addIcon("for_imagedialog",''),e.ui.registry.addIcon("for_imagealignleft_icon",''),e.ui.registry.addIcon("for_imagealigncenter_icon",''),e.ui.registry.addIcon("for_imagealignright_icon",''),e.ui.registry.addIcon("for_imagealignnone_icon",''),e.ui.registry.addIcon("for_imageswappool_icon",''),e.on("init",()=>{let t=e.getDoc();if(t){let a=t.createElement("style");a.setAttribute("data-for-images-plugin","1"),a.textContent=Z,t.head.appendChild(a)}let i=o.get(),s=D(i.widthPresets);e.getBody().querySelectorAll("figure").forEach(a=>{let u=a,v=u.querySelector("img");v&&u.classList.length>0&&F(v,s)}),f()}),e.on("SetContent",()=>{z(e),f()}),e.on("PastePostProcess",t=>{t!=null&&t.node&&z(e,t.node)}),e.on("ObjectResizeStart",t=>{var i;((i=t.target)==null?void 0:i.nodeName)==="IMG"&&t.preventDefault()}),e.options.set("object_resizing",!1);let m=e.options.isRegistered;typeof m=="function"&&m.call(e.options,"quickbars_image_toolbar")&&e.options.set("quickbars_image_toolbar",!1),e.options.set("image_caption",!1);let d=()=>{let t=p(e);if(!t)return null;let i=y(t);return i&&i.nodeName==="FIGURE"?i:null},L=()=>{let t=d();return t?(e.selection.select(t,!0),e.nodeChanged(),!0):!1},H=(t,i)=>{i.setData("text/html",t.outerHTML),i.setData("text/plain",t.textContent||"")},A=t=>{e.undoManager.transact(()=>{e.dom.remove(t)}),e.nodeChanged()};e.on("copy",t=>{let i=d();!i||!t.clipboardData||(H(i,t.clipboardData),t.preventDefault())}),e.on("cut",t=>{let i=d();!i||!t.clipboardData||(H(i,t.clipboardData),t.preventDefault(),A(i))}),e.on("keydown",t=>{let i=String(t.key||"").toLowerCase();if(i==="delete"||i==="backspace"){let s=d();if(s){t.preventDefault(),A(s);return}}(t.ctrlKey||t.metaKey)&&(i==="c"||i==="x")&&L()}),e.on("BeforeExecCommand",t=>{let i=String(t.command||"").toLowerCase();if(i==="copy"||i==="cut"){L();return}if(i==="delete"||i==="forwarddelete"||i==="mcedelete"){let s=d();if(!s)return;t.preventDefault(),A(s)}}),e.ui.registry.addMenuButton("imagewidth",{icon:"resize",tooltip:"Bildbreite",fetch:t=>{let i=p(e);if(!i){t([]);return}let s=o.get(),l=w(i,s.widthPresets);t(s.widthPresets.map(a=>({type:"menuitem",text:a.label,icon:l&&l.class===a.class?"checkmark":void 0,onAction:()=>b(e,i,a,s.widthPresets)})))}}),e.ui.registry.addToggleButton("for_imagealignleft",{icon:"for_imagealignleft_icon",tooltip:"Bild links",onAction:()=>{let t=p(e);if(!t)return;let i=o.get(),s=i.alignPresets.find(a=>a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")),l=w(t,i.alignPresets);l&&s&&l.class===s.class?b(e,t,{label:"",class:""},i.alignPresets):s&&b(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=o.get(),a=w(s,l.alignPresets);t.setActive(!!a&&(a.class.includes("float-left")||a.class.includes("float-start")||a.class.includes("align-left")||a.class.includes("uk-float-left")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealigncenter",{icon:"for_imagealigncenter_icon",tooltip:"Bild zentrieren",onAction:()=>{let t=p(e);if(!t)return;let i=o.get(),s=i.alignPresets.find(a=>a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")),l=w(t,i.alignPresets);l&&s&&l.class===s.class?b(e,t,{label:"",class:""},i.alignPresets):s&&b(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=o.get(),a=w(s,l.alignPresets);t.setActive(!!a&&(a.class.includes("margin-auto")||a.class.includes("mx-auto")||a.class.includes("align-center")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("for_imagealignright",{icon:"for_imagealignright_icon",tooltip:"Bild rechts",onAction:()=>{let t=p(e);if(!t)return;let i=o.get(),s=i.alignPresets.find(a=>a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")),l=w(t,i.alignPresets);l&&s&&l.class===s.class?b(e,t,{label:"",class:""},i.alignPresets):s&&b(e,t,s,i.alignPresets)},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=o.get(),a=w(s,l.alignPresets);t.setActive(!!a&&(a.class.includes("float-right")||a.class.includes("float-end")||a.class.includes("align-right")||a.class.includes("uk-float-right")))};return e.on("NodeChange",i),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("for_imagealignnone",{icon:"for_imagealignnone_icon",tooltip:"Ausrichtung entfernen",onAction:()=>{let t=p(e);if(!t)return;let i=o.get();b(e,t,{label:"",class:""},i.alignPresets)}}),e.ui.registry.addMenuButton("imageeffect",{icon:"color-picker",tooltip:"Bild-Effekte",fetch:t=>{let i=p(e);if(!i){t([]);return}let s=o.get(),l=T(i,s.effectPresets);t(s.effectPresets.map(a=>({type:"togglemenuitem",text:a.label,active:a.class===""?l.length===0:l.some(u=>u.class===a.class),onAction:()=>V(e,i,a,s.effectPresets)})))}}),e.ui.registry.addButton("imagewidthdialog",{icon:"for_imagedialog",tooltip:"Bildformatierung",onAction:()=>{let t=p(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let i=o.get(),s=w(t,i.widthPresets),l=w(t,i.alignPresets),a=T(t,i.effectPresets),u=i.widthPresets.map(h=>({text:h.label,value:h.class})),v=i.alignPresets.map(h=>({text:h.label,value:h.class})),E=i.effectPresets.filter(h=>h.class).map(h=>({type:"checkbox",name:"effect_"+h.class.replace(/\s+/g,"_"),label:h.label})),I={widthClass:(s==null?void 0:s.class)||"",alignClass:(l==null?void 0:l.class)||""};i.effectPresets.filter(h=>h.class).forEach(h=>{I["effect_"+h.class.replace(/\s+/g,"_")]=a.some(k=>k.class===h.class)});let N=[{type:"selectbox",name:"widthClass",label:"Breite",items:u},{type:"selectbox",name:"alignClass",label:"Ausrichtung",items:v}];E.length>0&&N.push({type:"bar",items:E}),e.windowManager.open({title:"Bildformatierung",size:"normal",body:{type:"panel",items:N},buttons:[{type:"cancel",name:"cancel",text:"Abbrechen"},{type:"submit",name:"save",text:"\xDCbernehmen",primary:!0}],initialData:I,onSubmit:h=>{let k=h.getData(),G=i.widthPresets.find(x=>x.class===k.widthClass)||{label:"",class:""};b(e,t,G,i.widthPresets);let O=i.alignPresets.find(x=>x.class===k.alignClass)||{label:"",class:""};b(e,t,O,i.alignPresets);let _=y(t);_&&(C(_,P(i.effectPresets),!1),i.effectPresets.filter(x=>x.class).forEach(x=>{k["effect_"+x.class.replace(/\s+/g,"_")]&&x.class.split(/\s+/).forEach(S=>{S&&_.classList.add(S)})})),e.nodeChanged(),e.undoManager.add(),h.close()}})}}),e.ui.registry.addContextToolbar("for_imagestoolbar",{predicate:t=>t.nodeName==="IMG"||t.nodeName==="FIGURE"&&t.querySelector("img"),items:"imagewidth for_imagealignleft for_imagealigncenter for_imagealignright imageeffect for_imagealignnone | imagealt imagecaption imageshowpool imageswappool",position:"node",scope:"node"}),e.ui.registry.addToggleButton("imagealt",{icon:"accessibility-check",tooltip:"Alt-Text bearbeiten",onAction:()=>{let t=p(e);if(!t)return;let i=t.getAttribute("alt")||"";e.windowManager.open({title:"Alt-Text (Bildbeschreibung)",body:{type:"panel",items:[{type:"input",name:"alttext",label:"Beschreibung f\xFCr Screenreader",placeholder:'z.B. "Rotes Auto vor blauem Himmel"'},{type:"htmlpanel",html:'

Beschreiben Sie den Bildinhalt kurz und pr\xE4gnant f\xFCr Menschen die das Bild nicht sehen k\xF6nnen.

'}]},initialData:{alttext:i},buttons:[{type:"cancel",text:"Abbrechen"},{type:"submit",text:"Speichern",primary:!0}],onSubmit:s=>{let l=s.getData();t.setAttribute("alt",l.alttext),e.nodeChanged(),e.undoManager.add(),s.close()}})},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=s.getAttribute("alt");t.setActive(!!l&&l.trim()!=="")};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addToggleButton("imagecaption",{icon:"comment",tooltip:"Bildunterschrift",onAction:()=>{var s;let t=p(e);if(!t)return;let i=y(t);if(i){let l=i.querySelector("figcaption");if(l)(s=l.textContent)!=null&&s.trim()?e.selection.setCursorLocation(l,0):l.remove();else{let u=e.getDoc().createElement("figcaption");u.innerHTML=" ",i.appendChild(u),e.selection.setCursorLocation(u,0)}}else{let l=M(e,t),u=e.getDoc().createElement("figcaption");u.innerHTML=" ",l.appendChild(u),e.selection.setCursorLocation(u,0)}e.nodeChanged(),e.undoManager.add()},onSetup:t=>{let i=()=>{let s=p(e);if(!s){t.setActive(!1);return}let l=y(s),a=(l==null?void 0:l.querySelector("figcaption"))!==null;t.setActive(a)};return e.on("NodeChange",i),i(),()=>e.off("NodeChange",i)}}),e.ui.registry.addButton("imageshowpool",{icon:"gallery",tooltip:"Zeige Medium im Medienpool",onAction:()=>{var l,a;let t=p(e),i=((a=(l=t==null?void 0:t.getAttribute("src"))==null?void 0:l.split("/").pop())==null?void 0:a.split("?")[0])||"";if(!i){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}let s="index.php?page=mediapool/media&file_name="+encodeURIComponent(i);window.open(s,"mediapool","width=1000,height=700,resizable=yes,scrollbars=yes")}}),e.ui.registry.addButton("imageswappool",{icon:"for_imageswappool_icon",tooltip:"Austauschen aus Medienpool",onAction:()=>{let t=p(e);if(!t){e.notificationManager.open({text:"Bitte zuerst ein Bild ausw\xE4hlen.",type:"warning",timeout:3e3});return}try{e.selection.select(t)}catch(s){}if(!(e.plugins&&e.plugins.image||typeof e.queryCommandSupported=="function"&&e.queryCommandSupported("mceImage"))){e.notificationManager.open({text:'Bild-Dialog nicht verf\xFCgbar \u2013 bitte das Plugin "image" im Profil aktivieren.',type:"warning",timeout:5e3});return}e.execCommand("mceImage")}}),e.on("NodeChange",()=>{let t=p(e);if(!t)return;let i=o.get(),s=w(t,i.widthPresets),l=w(t,i.alignPresets),a=T(t,i.effectPresets),u=[];s&&s.class&&u.push(s.label),l&&l.class&&u.push(l.label),a.length>0&&u.push(a.map(E=>E.label).join(", "));let v=y(t)||t;u.length>0?v.setAttribute("title",u.join(" \xB7 ")):v.removeAttribute("title")})},R=()=>{tinymce.PluginManager.add("for_images",X)};R();})(); diff --git a/assets/vendor/tinymce/plugins/cleanpaste/CHANGELOG.txt b/assets/vendor/tinymce/plugins/cleanpaste/CHANGELOG.txt deleted file mode 100644 index 603de667..00000000 --- a/assets/vendor/tinymce/plugins/cleanpaste/CHANGELOG.txt +++ /dev/null @@ -1,10 +0,0 @@ -Changelog for Clean Paste Plugin - -1.0.0 (2026-04-22) - - Initial release - - Automatic cleanup of pasted content from MS Office, Google Docs and other sources - - Configurable via REDAXO TinyMCE addon settings page - - Supports positive list for preserved CSS classes (exact names and regex patterns) - - Supports positive list for preserved inline style properties - - Configurable limits for consecutive
tags and empty paragraphs - - Optional allowed-tags filter (unwrap disallowed tags, keep content) diff --git a/assets/vendor/tinymce/plugins/cleanpaste/LICENSE.txt b/assets/vendor/tinymce/plugins/cleanpaste/LICENSE.txt deleted file mode 100644 index 52945f0d..00000000 --- a/assets/vendor/tinymce/plugins/cleanpaste/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -Clean Paste Plugin for TinyMCE -Copyright (c) 2026 Friends Of REDAXO - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/assets/vendor/tinymce/plugins/cleanpaste/plugin.js b/assets/vendor/tinymce/plugins/cleanpaste/plugin.js deleted file mode 100644 index 3bd8a0d3..00000000 --- a/assets/vendor/tinymce/plugins/cleanpaste/plugin.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict";(()=>{var w=Object.defineProperty;var m=Object.getOwnPropertySymbols;var A=Object.prototype.hasOwnProperty,v=Object.prototype.propertyIsEnumerable;var d=(e,t,r)=>t in e?w(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,g=(e,t)=>{for(var r in t||(t={}))A.call(t,r)&&d(e,r,t[r]);if(m)for(var r of m(t))v.call(t,r)&&d(e,r,t[r]);return e};var P={strip_ms_office:!0,strip_google_docs:!0,remove_styles:!0,preserve_styles:[],remove_classes:!0,preserve_classes:[],remove_ids:!0,remove_data_attrs:!0,max_br:2,max_empty_paragraphs:2,allowed_tags:[],clean_internal_paste:!1};function _(e,t){for(let r of t)if(r.trim()!==""){if(r.startsWith("regex:"))try{if(new RegExp(r.slice(6),"i").test(e))return!0}catch(a){}else if(e.trim()===r.trim())return!0}return!1}function y(e){return/^for-/.test(e)||e==="media"?!0:/^img-/.test(e)||/^uk-(width|float|margin|display|box-shadow|border-rounded)/.test(e)||/^(float-(start|end)|mx-auto|w-100)$/.test(e)||/^col-/.test(e)}function x(e){return/^data-for-/.test(e)||e==="data-mce-selected"}function S(e){return e=e.replace(//gi,""),e=e.replace(/<\/?[a-z]:[^>]*>/gi,""),e=e.replace(/[\s\S]*?<\/xml>/gi,""),e=e.replace(//gi,""),e=e.replace(new RegExp("","gs"),""),e}function E(e){return e=e.replace(//gi,""),e=e.replace(//gi,""),e=e.replace(/\s+id="docs-internal-guid-[^"]*"/gi,""),e}function k(e){let t=e;for(;t&&t.nodeType===1;){if(t.tagName&&t.tagName.toLowerCase()==="figure"&&(t.getAttribute("class")||"").split(/\s+/).filter(Boolean).some(i=>y(i))||t.tagName&&t.tagName.toLowerCase()==="oembed")return!0;t=t.parentElement}return!1}function b(e,t,r){var p,o,u;if(k(e))return;let a=Array.from(e.children);for(let l of a)b(l,t,r);let i=e.tagName.toLowerCase();if(t.allowed_tags.length>0&&!t.allowed_tags.some(s=>s.toLowerCase()===i)){let s=r.createDocumentFragment();for(;e.firstChild;)s.appendChild(e.firstChild);(p=e.parentNode)==null||p.replaceChild(s,e);return}if(t.remove_ids&&e.removeAttribute("id"),t.remove_data_attrs){let l=Array.from(e.attributes).map(s=>s.name).filter(s=>s.startsWith("data-")&&!x(s));for(let s of l)e.removeAttribute(s)}if(e.hasAttribute("style")){let l=(o=e.getAttribute("style"))!=null?o:"",s=[];t.remove_styles?s=l.split(";").map(n=>n.trim()).filter(Boolean).filter(n=>{let c=n.indexOf(":"),f=c>=0?n.slice(0,c).trim():n;return _(f,t.preserve_styles)}):s=l.split(";").map(n=>n.trim()).filter(Boolean),t.strip_ms_office&&(s=s.filter(n=>{let c=n.indexOf(":"),f=c>=0?n.slice(0,c).trim().toLowerCase():n.toLowerCase();return!f.startsWith("mso-")&&f!=="tab-stop-num"&&f!=="-webkit-tap-highlight-color"})),s.length>0?e.setAttribute("style",s.join("; ")):e.removeAttribute("style")}if(t.remove_classes&&e.hasAttribute("class")){let n=((u=e.getAttribute("class"))!=null?u:"").split(/\s+/).filter(Boolean).filter(c=>y(c)||_(c,t.preserve_classes));n.length>0?e.setAttribute("class",n.join(" ")):e.removeAttribute("class")}t.strip_ms_office&&i!=="html"&&(e.removeAttribute("lang"),e.removeAttribute("xml:lang"))}function F(e,t){if(t.max_br>0){let r=`(?:\\s*){${t.max_br+1},}`,a="
".repeat(t.max_br);e=e.replace(new RegExp(r,"gi"),a)}if(t.max_empty_paragraphs>0){let r=`(?:]*>\\s*(?:| )*\\s*<\\/p>\\s*){${t.max_empty_paragraphs+1},}`,a=`

 

-`.repeat(t.max_empty_paragraphs);e=e.replace(new RegExp(r,"gi"),a)}return e=e.replace(/(\r\n|\r|\n){3,}/g,` - -`),e}function h(e,t){t.strip_ms_office&&(e=S(e)),t.strip_google_docs&&(e=E(e));let a=new DOMParser().parseFromString(""+e+"","text/html"),i=a.body;return b(i,t,a),e=i.innerHTML,e=F(e,t),e}function L(){var i,p;let e=new Set,r=(p=(i=window.rex)==null?void 0:i.tinyGlobalOptions)==null?void 0:p.style_formats;if(!Array.isArray(r))return[];let a=o=>{var c;if(!o||typeof o!="object")return;let u=(c=o.format)!=null?c:o;if(!u)return;let l=u.classes;typeof l=="string"?l.split(/\s+/).filter(Boolean).forEach(f=>e.add(f)):Array.isArray(l)&&l.forEach(f=>{typeof f=="string"&&e.add(f)});let s=u.items;Array.isArray(s)&&s.forEach(a);let n=o.items;Array.isArray(n)&&n.forEach(a)};return r.forEach(a),Array.from(e)}var N=()=>{tinymce.PluginManager.add("cleanpaste",e=>{var i,p;let t=(p=typeof tinyCleanPasteConfig!="undefined"?tinyCleanPasteConfig:null)!=null?p:(i=window.tinyCleanPasteConfig)!=null?i:{},r=g(g({},P),t),a=L();if(a.length>0){let o=new Set([...r.preserve_classes,...a]);r.preserve_classes=Array.from(o)}e.on("PastePreProcess",o=>{o.internal&&!r.clean_internal_paste||o.content&&(o.content=h(o.content,r))})})},C=N;C();})(); diff --git a/assets/vendor/tinymce/plugins/cleanpaste/plugin.min.js b/assets/vendor/tinymce/plugins/cleanpaste/plugin.min.js deleted file mode 100644 index 3bd8a0d3..00000000 --- a/assets/vendor/tinymce/plugins/cleanpaste/plugin.min.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict";(()=>{var w=Object.defineProperty;var m=Object.getOwnPropertySymbols;var A=Object.prototype.hasOwnProperty,v=Object.prototype.propertyIsEnumerable;var d=(e,t,r)=>t in e?w(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,g=(e,t)=>{for(var r in t||(t={}))A.call(t,r)&&d(e,r,t[r]);if(m)for(var r of m(t))v.call(t,r)&&d(e,r,t[r]);return e};var P={strip_ms_office:!0,strip_google_docs:!0,remove_styles:!0,preserve_styles:[],remove_classes:!0,preserve_classes:[],remove_ids:!0,remove_data_attrs:!0,max_br:2,max_empty_paragraphs:2,allowed_tags:[],clean_internal_paste:!1};function _(e,t){for(let r of t)if(r.trim()!==""){if(r.startsWith("regex:"))try{if(new RegExp(r.slice(6),"i").test(e))return!0}catch(a){}else if(e.trim()===r.trim())return!0}return!1}function y(e){return/^for-/.test(e)||e==="media"?!0:/^img-/.test(e)||/^uk-(width|float|margin|display|box-shadow|border-rounded)/.test(e)||/^(float-(start|end)|mx-auto|w-100)$/.test(e)||/^col-/.test(e)}function x(e){return/^data-for-/.test(e)||e==="data-mce-selected"}function S(e){return e=e.replace(//gi,""),e=e.replace(/<\/?[a-z]:[^>]*>/gi,""),e=e.replace(/[\s\S]*?<\/xml>/gi,""),e=e.replace(//gi,""),e=e.replace(new RegExp("","gs"),""),e}function E(e){return e=e.replace(//gi,""),e=e.replace(//gi,""),e=e.replace(/\s+id="docs-internal-guid-[^"]*"/gi,""),e}function k(e){let t=e;for(;t&&t.nodeType===1;){if(t.tagName&&t.tagName.toLowerCase()==="figure"&&(t.getAttribute("class")||"").split(/\s+/).filter(Boolean).some(i=>y(i))||t.tagName&&t.tagName.toLowerCase()==="oembed")return!0;t=t.parentElement}return!1}function b(e,t,r){var p,o,u;if(k(e))return;let a=Array.from(e.children);for(let l of a)b(l,t,r);let i=e.tagName.toLowerCase();if(t.allowed_tags.length>0&&!t.allowed_tags.some(s=>s.toLowerCase()===i)){let s=r.createDocumentFragment();for(;e.firstChild;)s.appendChild(e.firstChild);(p=e.parentNode)==null||p.replaceChild(s,e);return}if(t.remove_ids&&e.removeAttribute("id"),t.remove_data_attrs){let l=Array.from(e.attributes).map(s=>s.name).filter(s=>s.startsWith("data-")&&!x(s));for(let s of l)e.removeAttribute(s)}if(e.hasAttribute("style")){let l=(o=e.getAttribute("style"))!=null?o:"",s=[];t.remove_styles?s=l.split(";").map(n=>n.trim()).filter(Boolean).filter(n=>{let c=n.indexOf(":"),f=c>=0?n.slice(0,c).trim():n;return _(f,t.preserve_styles)}):s=l.split(";").map(n=>n.trim()).filter(Boolean),t.strip_ms_office&&(s=s.filter(n=>{let c=n.indexOf(":"),f=c>=0?n.slice(0,c).trim().toLowerCase():n.toLowerCase();return!f.startsWith("mso-")&&f!=="tab-stop-num"&&f!=="-webkit-tap-highlight-color"})),s.length>0?e.setAttribute("style",s.join("; ")):e.removeAttribute("style")}if(t.remove_classes&&e.hasAttribute("class")){let n=((u=e.getAttribute("class"))!=null?u:"").split(/\s+/).filter(Boolean).filter(c=>y(c)||_(c,t.preserve_classes));n.length>0?e.setAttribute("class",n.join(" ")):e.removeAttribute("class")}t.strip_ms_office&&i!=="html"&&(e.removeAttribute("lang"),e.removeAttribute("xml:lang"))}function F(e,t){if(t.max_br>0){let r=`(?:\\s*){${t.max_br+1},}`,a="
".repeat(t.max_br);e=e.replace(new RegExp(r,"gi"),a)}if(t.max_empty_paragraphs>0){let r=`(?:]*>\\s*(?:| )*\\s*<\\/p>\\s*){${t.max_empty_paragraphs+1},}`,a=`

 

-`.repeat(t.max_empty_paragraphs);e=e.replace(new RegExp(r,"gi"),a)}return e=e.replace(/(\r\n|\r|\n){3,}/g,` - -`),e}function h(e,t){t.strip_ms_office&&(e=S(e)),t.strip_google_docs&&(e=E(e));let a=new DOMParser().parseFromString(""+e+"","text/html"),i=a.body;return b(i,t,a),e=i.innerHTML,e=F(e,t),e}function L(){var i,p;let e=new Set,r=(p=(i=window.rex)==null?void 0:i.tinyGlobalOptions)==null?void 0:p.style_formats;if(!Array.isArray(r))return[];let a=o=>{var c;if(!o||typeof o!="object")return;let u=(c=o.format)!=null?c:o;if(!u)return;let l=u.classes;typeof l=="string"?l.split(/\s+/).filter(Boolean).forEach(f=>e.add(f)):Array.isArray(l)&&l.forEach(f=>{typeof f=="string"&&e.add(f)});let s=u.items;Array.isArray(s)&&s.forEach(a);let n=o.items;Array.isArray(n)&&n.forEach(a)};return r.forEach(a),Array.from(e)}var N=()=>{tinymce.PluginManager.add("cleanpaste",e=>{var i,p;let t=(p=typeof tinyCleanPasteConfig!="undefined"?tinyCleanPasteConfig:null)!=null?p:(i=window.tinyCleanPasteConfig)!=null?i:{},r=g(g({},P),t),a=L();if(a.length>0){let o=new Set([...r.preserve_classes,...a]);r.preserve_classes=Array.from(o)}e.on("PastePreProcess",o=>{o.internal&&!r.clean_internal_paste||o.content&&(o.content=h(o.content,r))})})},C=N;C();})(); diff --git a/assets/vendor/tinymce/plugins/cleanpaste/version.txt b/assets/vendor/tinymce/plugins/cleanpaste/version.txt deleted file mode 100644 index ea275684..00000000 --- a/assets/vendor/tinymce/plugins/cleanpaste/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.0.0-0 \ No newline at end of file diff --git a/assets/vendor/tinymce/plugins/for_a11y/plugin.js b/assets/vendor/tinymce/plugins/for_a11y/plugin.js deleted file mode 100644 index 02523bfb..00000000 --- a/assets/vendor/tinymce/plugins/for_a11y/plugin.js +++ /dev/null @@ -1,331 +0,0 @@ -"use strict";(()=>{var Q=Object.defineProperty;var q=Object.getOwnPropertySymbols;var j=Object.prototype.hasOwnProperty,J=Object.prototype.propertyIsEnumerable;var I=(e,r,t)=>r in e?Q(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t,N=(e,r)=>{for(var t in r||(r={}))j.call(r,t)&&I(e,t,r[t]);if(q)for(var t of q(r))J.call(r,t)&&I(e,t,r[t]);return e};var ee=["hier","klick hier","klicken","klicke hier","klicken sie hier","mehr","mehr erfahren","mehr lesen","weiter","weiterlesen","weiter lesen","link","dieser link","mehr infos","infos","read more","click here","more","here","learn more","details"],R={"img-missing-alt":!0,"img-alt-in-text-link":!0,"img-empty-alt-nondeco":!0,"img-alt-too-long":!0,"img-alt-filename":!0,"img-alt-redundant":!0,"link-generic-text":!0,"link-no-accname":!0,"link-new-window":!0,"link-raw-url":!0,"link-duplicate-text":!0,"link-file-no-format":!0,"heading-empty":!0,"heading-skip":!0,"heading-allcaps":!0,"text-bold-as-heading":!0,"text-bold-too-long":!0,"text-too-many-spaces":!0,"list-fake":!0,"list-single-item":!0,"blank-paragraphs":!0,"table-no-th":!0,"table-no-caption":!0,"table-th-no-scope":!0,"iframe-no-title":!0},M={error:0,warn:1,info:2},D={error:"Fehler",warn:"Warnung",info:"Hinweis"},P={error:"\u26D4",warn:"\u26A0\uFE0F",info:"\u2139\uFE0F"};function te(e){try{let r=e.getParam("a11y_rules");if(r&&typeof r=="object")return N(N({},R),r)}catch(r){}return R}function re(e){try{let r=e.getParam("a11y_generic_link_texts");if(Array.isArray(r)&&r.length)return r.map(t=>String(t).toLowerCase().trim()).filter(Boolean)}catch(r){}return ee}function ne(e){try{let r=e.getParam("a11y_new_window_warning");if(typeof r=="boolean")return r}catch(r){}return!0}function A(e,r=80){let t=e.replace(/\s+/g," ").trim();return t.length>r?t.slice(0,r-1)+"\u2026":t}function h(e){let r=e.tagName.toLowerCase(),t=A(e.textContent||"",60);if(r==="img"){let l=e.getAttribute("src")||"",o=e.getAttribute("alt");return`${A(o,20)}`}if(r==="a"){let l=e.getAttribute("href")||"";return`${t||"(leer)"}`}if(r==="iframe"){let l=e.getAttribute("src")||"";return`