Skip to content

Commit e5b2b39

Browse files
committed
Release 8.1.0: Style-Sets für CSS-Framework-Styles
- Neue Style-Sets Verwaltung für UIkit 3, Bootstrap 5 und eigene Styles - Profil-Zuordnung: Style-Sets können einzelnen Profilen zugewiesen werden - Import/Export von Style-Sets als JSON - Demo-Sets für UIkit 3 und Bootstrap 5 vorinstallierbar - Verbesserter Styles-Button mit verschachtelten Menüs - Format-Menü Integration - Fix: Button-Styles verwenden korrekt selector statt inline für Links - Fix: Eindeutige Format-Namen verhindern Kollisionen - Fix: CSS-Ladereihenfolge korrigiert
1 parent a5605f5 commit e5b2b39

File tree

13 files changed

+1248
-37
lines changed

13 files changed

+1248
-37
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
Changelog
22
=========
33

4+
Version 8.1.0
5+
-------------------------------
6+
7+
### Neue Features
8+
* **Style-Sets:** Neue zentrale Verwaltung von CSS-Framework-spezifischen Styles.
9+
* UIkit 3, Bootstrap 5 und eigene Style-Definitionen.
10+
* Profil-Zuordnung: Style-Sets können einzelnen Profilen zugewiesen werden.
11+
* Import/Export von Style-Sets als JSON.
12+
* Demo-Sets für UIkit 3 und Bootstrap 5 vorinstallierbar.
13+
* **Verbesserter Styles-Button:** Eigener "stylesets" Button mit vollständiger Unterstützung für verschachtelte Menüs.
14+
* **Format-Menü Integration:** Style-Sets sind auch über das Format-Menü erreichbar.
15+
16+
### Bugfixes
17+
* Fix: Button-Styles verwenden nun korrekt `selector` statt `inline` für `<a>`-Elemente.
18+
* Fix: Eindeutige Format-Namen verhindern Kollisionen zwischen Buttons, Backgrounds, Cards etc.
19+
* Fix: CSS-Ladereihenfolge korrigiert (Profil-CSS überschreibt globale Styles).
20+
21+
### Verbesserungen
22+
* Style-Sets werden über die Datenbank verwaltet (`rex_tinymce_stylesets`).
23+
* Extension Point `TINYMCE_GLOBAL_OPTIONS` für globale TinyMCE-Optionen.
24+
* Bessere Debug-Ausgaben in der Browser-Konsole.
25+
426
Version 8.0.0
527
-------------------------------
628

DEVS.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,62 @@ Das Snippets-Plugin lädt seine Daten dynamisch über eine REDAXO-API-Funktion.
9292
* **Aufruf:** `index.php?rex-api-call=tinymce_get_snippets`
9393
* **Rückgabe:** JSON-Array mit Objekten `{title: "Name", content: "HTML-Inhalt"}`
9494

95+
### Style-Sets System
96+
97+
Style-Sets werden in der Datenbank-Tabelle `rex_tinymce_stylesets` gespeichert und über `TINYMCE_GLOBAL_OPTIONS` an alle Profile weitergegeben.
98+
99+
#### Extension Point `TINYMCE_GLOBAL_OPTIONS`
100+
101+
Dieser EP wird beim Laden der Backend-Assets aufgerufen und ermöglicht das Hinzufügen globaler TinyMCE-Optionen:
102+
103+
```php
104+
rex_extension::register('TINYMCE_GLOBAL_OPTIONS', function (rex_extension_point $ep) {
105+
$options = $ep->getSubject();
106+
107+
// Beispiel: Zusätzliches CSS hinzufügen
108+
$options['content_css'][] = ['url' => '/my/custom.css', 'profiles' => ['my_profile']];
109+
110+
// Beispiel: Zusätzliche Style-Formats
111+
$options['style_formats'][] = [
112+
'format' => ['title' => 'Mein Stil', 'inline' => 'span', 'classes' => 'my-class'],
113+
'profiles' => ['my_profile'] // Leer = alle Profile
114+
];
115+
116+
return $options;
117+
});
118+
```
119+
120+
#### Profil-Filterung
121+
122+
Style-Sets können auf bestimmte Profile beschränkt werden:
123+
124+
* **Leeres `profiles`-Array:** Style-Set gilt für alle Profile
125+
* **Gefülltes `profiles`-Array:** Style-Set gilt nur für die angegebenen Profile
126+
127+
Die Filterung erfolgt client-seitig in `base.js` beim Initialisieren des Editors.
128+
129+
#### Eigene Style-Sets programmatisch anlegen
130+
131+
```php
132+
$sql = rex_sql::factory();
133+
$sql->setTable(rex::getTable('tinymce_stylesets'));
134+
$sql->setValue('name', 'mein_styleset');
135+
$sql->setValue('description', 'Meine Custom Styles');
136+
$sql->setValue('content_css', '/assets/css/my-framework.css');
137+
$sql->setValue('style_formats', json_encode([
138+
[
139+
'title' => 'Meine Styles',
140+
'items' => [
141+
['title' => 'Highlight', 'name' => 'my-highlight', 'inline' => 'span', 'classes' => 'highlight']
142+
]
143+
]
144+
]));
145+
$sql->setValue('profiles', 'profil1, profil2'); // Leer = alle
146+
$sql->setValue('active', 1);
147+
$sql->setValue('prio', 10);
148+
$sql->insert();
149+
```
150+
95151
### Link YForm Plugin Internals
96152

97153
Das `link_yform` Plugin speichert Links zu YForm-Datensätzen als interne Platzhalter.

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,49 @@ Unter "TinyMCE" -> "Snippets" können Sie beliebige HTML-Schnipsel anlegen, bear
114114
2. Fügen Sie den Button `snippets` zur Toolbar hinzu.
115115
3. Im Editor erscheint nun ein Dropdown-Menü, über das Sie die angelegten Snippets in den Text einfügen können.
116116

117+
## Style-Sets (CSS-Framework Styles)
118+
119+
Style-Sets ermöglichen die zentrale Verwaltung von CSS-Framework-spezifischen Styles wie UIkit 3 oder Bootstrap 5.
120+
121+
### Verwaltung
122+
Unter "TinyMCE" -> "Style-Sets" können Sie:
123+
- Eigene Style-Sets anlegen und bearbeiten
124+
- Demo-Sets für UIkit 3 und Bootstrap 5 installieren
125+
- Style-Sets importieren und exportieren (JSON)
126+
127+
### Profil-Zuordnung
128+
Style-Sets können einzelnen Profilen zugewiesen werden:
129+
- **Leer** = Style-Set gilt für alle Profile
130+
- **Profilnamen** = Komma-getrennte Liste (z.B. `uikit, bootstrap-full`)
131+
132+
So können UIkit-Styles nur im UIkit-Profil erscheinen und Bootstrap-Styles nur im Bootstrap-Profil.
133+
134+
### Aufbau eines Style-Sets
135+
- **Name**: Eindeutiger Bezeichner
136+
- **Content CSS**: URL zum CSS-Framework (z.B. CDN-Link zu UIkit oder Bootstrap)
137+
- **Style Formats**: JSON-Array mit TinyMCE style_formats Definitionen
138+
- **Profile**: Optionale Zuordnung zu bestimmten Profilen
139+
140+
### Beispiel Style Format (JSON)
141+
```json
142+
[
143+
{
144+
"title": "Buttons",
145+
"items": [
146+
{"title": "Primary", "name": "uk-button-primary", "selector": "a", "classes": "uk-button uk-button-primary"},
147+
{"title": "Secondary", "name": "uk-button-secondary", "selector": "a", "classes": "uk-button uk-button-secondary"}
148+
]
149+
}
150+
]
151+
```
152+
153+
**Wichtige Format-Typen:**
154+
- `selector`: Wendet Klassen auf existierende Elemente an (z.B. `"selector": "a"` für Links)
155+
- `block`: Erstellt ein Block-Element (z.B. `"block": "div"`)
156+
- `inline`: Erstellt ein Inline-Element (z.B. `"inline": "span"`)
157+
- `name`: Eindeutiger interner Name (verhindert Kollisionen)
158+
- `wrapper`: Bei `true` wird das Element um die Auswahl gewickelt
159+
117160
## Link YForm Plugin
118161

119162
Das `link_yform` Plugin ermöglicht es, Datensätze aus YForm-Tabellen direkt im Editor zu verlinken.

assets/scripts/base.js

Lines changed: 200 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,113 @@ function tiny_init(container) {
9898
}
9999
}
100100

101+
// IMPORTANT: Merge global options FIRST before any other processing
102+
// This ensures style_formats are available when TinyMCE registers the 'styles' button
103+
if (typeof rex !== 'undefined' && rex.tinyGlobalOptions) {
104+
let globalOpts = rex.tinyGlobalOptions;
105+
106+
// Debug: Log global options
107+
console.log('TinyMCE Global Options for profile "' + profile + '":', globalOpts);
108+
109+
// Helper function to check if a Style-Set applies to this profile
110+
// Empty profiles array means it applies to ALL profiles
111+
function appliesToProfile(profilesList) {
112+
if (!profilesList || profilesList.length === 0) {
113+
return true; // Empty = applies to all profiles
114+
}
115+
return profilesList.indexOf(profile) !== -1;
116+
}
117+
118+
// Merge content_css (array) - filter by profile
119+
// New format: [{url: "...", profiles: ["uikit", "bootstrap"]}]
120+
// Legacy format: ["url1", "url2"]
121+
if (globalOpts.content_css && globalOpts.content_css.length > 0) {
122+
let filteredCss = [];
123+
globalOpts.content_css.forEach(function(item) {
124+
if (typeof item === 'string') {
125+
// Legacy format - always include
126+
filteredCss.push(item);
127+
} else if (item.url && appliesToProfile(item.profiles)) {
128+
// New format with profile filter
129+
filteredCss.push(item.url);
130+
}
131+
});
132+
133+
if (filteredCss.length > 0) {
134+
if (!options.content_css) {
135+
options.content_css = filteredCss;
136+
} else if (typeof options.content_css === 'string') {
137+
// Profile CSS at the end to override global styles
138+
options.content_css = filteredCss.concat([options.content_css]);
139+
} else {
140+
// Profile CSS array at the end to override global styles
141+
options.content_css = filteredCss.concat(options.content_css);
142+
}
143+
}
144+
}
145+
146+
// Merge content_style (string) - fixes focus outlines for UIkit/Bootstrap
147+
if (globalOpts.content_style) {
148+
if (!options.content_style) {
149+
options.content_style = globalOpts.content_style;
150+
} else {
151+
// Append global styles to existing content_style
152+
options.content_style = globalOpts.content_style + ' ' + options.content_style;
153+
}
154+
}
155+
156+
// Merge style_formats - filter by profile
157+
// New format: [{format: {...}, profiles: ["uikit"]}]
158+
// Legacy format: [{title: "...", items: [...]}]
159+
if (globalOpts.style_formats && globalOpts.style_formats.length > 0) {
160+
let filteredFormats = [];
161+
globalOpts.style_formats.forEach(function(item) {
162+
if (item.format && appliesToProfile(item.profiles)) {
163+
// New format with profile filter
164+
filteredFormats.push(item.format);
165+
} else if (item.title) {
166+
// Legacy format (has title = is a format group) - always include
167+
filteredFormats.push(item);
168+
}
169+
});
170+
171+
if (filteredFormats.length > 0) {
172+
// Enable merging with default formats (Headings, Inline, Blocks, Align)
173+
options.style_formats_merge = true;
174+
175+
if (!options.style_formats) {
176+
options.style_formats = [];
177+
}
178+
// Append filtered style formats to existing ones
179+
options.style_formats = options.style_formats.concat(filteredFormats);
180+
181+
// Replace 'styles' with 'stylesets' in toolbar (our custom button)
182+
if (options.toolbar && typeof options.toolbar === 'string') {
183+
// Replace existing 'styles' with 'stylesets'
184+
options.toolbar = options.toolbar.replace(/\bstyles\b/g, 'stylesets');
185+
// If neither exists, add stylesets at the beginning
186+
if (options.toolbar.indexOf('stylesets') === -1) {
187+
options.toolbar = 'stylesets ' + options.toolbar;
188+
}
189+
}
190+
191+
// Add stylesets to Format menu
192+
if (!options.menu) {
193+
options.menu = {};
194+
}
195+
options.menu.format = {
196+
title: 'Format',
197+
items: 'bold italic underline strikethrough superscript subscript codeformat | stylesets blocks fontfamily fontsize align lineheight | forecolor backcolor | removeformat'
198+
};
199+
200+
// Debug: Log filtered style_formats
201+
console.log('TinyMCE style_formats for profile "' + profile + '":', filteredFormats.length, 'items');
202+
}
203+
}
204+
} else {
205+
console.log('TinyMCE: No rex.tinyGlobalOptions found');
206+
}
207+
101208
// Merge external plugins from PluginRegistry into profile options
102209
// First try rex.tinyExternalPlugins (set via PHP at runtime), fallback to global tinyExternalPlugins from profiles.js
103210
let externalPluginsSource = (typeof rex !== 'undefined' && rex.tinyExternalPlugins) ? rex.tinyExternalPlugins :
@@ -127,42 +234,103 @@ function tiny_init(container) {
127234
}
128235
}
129236

130-
// Merge global options from TINYMCE_GLOBAL_OPTIONS extension point
131-
// This allows addons to add content_css, style_formats etc. to all profiles
132-
if (typeof rex !== 'undefined' && rex.tinyGlobalOptions) {
133-
let globalOpts = rex.tinyGlobalOptions;
134-
135-
// Merge content_css (array)
136-
if (globalOpts.content_css && globalOpts.content_css.length > 0) {
137-
if (!options.content_css) {
138-
options.content_css = [];
139-
} else if (typeof options.content_css === 'string') {
140-
options.content_css = [options.content_css];
237+
// Store the original setup function if it exists
238+
let originalSetup = options['setup'] || null;
239+
240+
// Create a new setup function that handles editor events and calls the original
241+
options['setup'] = function(editor) {
242+
// Register custom Style-Sets button and menu item
243+
if (options.style_formats && options.style_formats.length > 0) {
244+
245+
// Helper function to build menu items recursively
246+
function buildMenuItems(formats) {
247+
let items = [];
248+
formats.forEach(function(format) {
249+
if (format.items) {
250+
// It's a submenu/group
251+
items.push({
252+
type: 'nestedmenuitem',
253+
text: format.title,
254+
getSubmenuItems: function() {
255+
return buildMenuItems(format.items);
256+
}
257+
});
258+
} else {
259+
// It's a format item
260+
let formatName = format.name || format.format || 'custom-' + format.title.toLowerCase().replace(/\s+/g, '-');
261+
items.push({
262+
type: 'togglemenuitem',
263+
text: format.title,
264+
onAction: function() {
265+
editor.formatter.toggle(formatName);
266+
},
267+
onSetup: function(api) {
268+
let callback = function() {
269+
api.setActive(editor.formatter.match(formatName));
270+
};
271+
editor.on('NodeChange', callback);
272+
return function() {
273+
editor.off('NodeChange', callback);
274+
};
275+
}
276+
});
277+
}
278+
});
279+
return items;
141280
}
142-
// Prepend global CSS so it loads first
143-
options.content_css = globalOpts.content_css.concat(options.content_css);
281+
282+
// Register toolbar button 'stylesets'
283+
editor.ui.registry.addMenuButton('stylesets', {
284+
text: 'Styles',
285+
tooltip: 'Style-Sets',
286+
fetch: function(callback) {
287+
callback(buildMenuItems(options.style_formats));
288+
}
289+
});
290+
291+
// Register menu item 'stylesets' for Format menu
292+
editor.ui.registry.addNestedMenuItem('stylesets', {
293+
text: 'Style-Sets',
294+
getSubmenuItems: function() {
295+
return buildMenuItems(options.style_formats);
296+
}
297+
});
298+
299+
// Register all formats so they work when applied
300+
editor.on('init', function() {
301+
function registerFormats(formats) {
302+
formats.forEach(function(format) {
303+
if (format.items) {
304+
registerFormats(format.items);
305+
} else if (format.inline || format.block || format.selector) {
306+
let formatName = format.name || format.format || 'custom-' + format.title.toLowerCase().replace(/\s+/g, '-');
307+
editor.formatter.register(formatName, {
308+
inline: format.inline,
309+
block: format.block,
310+
selector: format.selector,
311+
classes: format.classes,
312+
styles: format.styles,
313+
attributes: format.attributes,
314+
wrapper: format.wrapper
315+
});
316+
}
317+
});
318+
}
319+
registerFormats(options.style_formats);
320+
console.log('TinyMCE: Registered', options.style_formats.length, 'style format groups');
321+
});
144322
}
145323

146-
// Merge style_formats
147-
if (globalOpts.style_formats && globalOpts.style_formats.length > 0) {
148-
if (globalOpts.style_formats_merge) {
149-
options.style_formats_merge = true;
150-
}
151-
if (!options.style_formats) {
152-
options.style_formats = [];
153-
}
154-
// Append global style formats
155-
options.style_formats = options.style_formats.concat(globalOpts.style_formats);
324+
// Set up default change handler
325+
editor.on('change', function(e) {
326+
$(editor.targetElm).html(editor.getContent());
327+
});
328+
329+
// Call original setup if it existed
330+
if (originalSetup && typeof originalSetup === 'function') {
331+
originalSetup(editor);
156332
}
157-
}
158-
159-
if (!options.hasOwnProperty('setup')) {
160-
options['setup'] = function(editor) {
161-
editor.on('change', function(e) {
162-
$(editor.targetElm).html(editor.getContent());
163-
})
164-
};
165-
}
333+
};
166334

167335
if (!options.hasOwnProperty('selector')) {
168336
options['selector'] = '.tiny-editor[data-profile="' + profile + '"]:not(.mce-initialized)';

0 commit comments

Comments
 (0)