Skip to content

Commit d9f6ee1

Browse files
committed
feat(stylesets): Profile-Gruppen, Kontextmenü, Vorschau-Beschreibung – Version 8.9.2
- Stylesets werden nach Profil und Styleset-Name gruppiert (Separator-Einträge) - Stylesets im Kontextmenü automatisch eingebunden - Profile-Assistent: Stylesets als wählbare Toolbar-Elemente - Vorschau-Modal zeigt jetzt Beschreibung des Profils an - Version auf 8.9.2 erhöht, Changelog ergänzt
1 parent 813f8a2 commit d9f6ee1

9 files changed

Lines changed: 234 additions & 36 deletions

File tree

CHANGELOG.md

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

4+
Version 8.9.2
5+
-------------------------------
6+
7+
### Stylesets: Profil-Gruppierung und Kontext-Menü-Integration
8+
9+
* **Hierarchische Stylesets-Struktur**: Styles werden jetzt nach Profil und Styleset-Zuweisung gruppiert mit visuellen Trennlinien angezeigt.
10+
* **Trennlinien mit Labels**: Separator-Items zeigen das Styleset-Name und zugehörige Profile (z.B. "─ UIkit3 (light) ─") für bessere Übersichtlichkeit.
11+
* **Kontext-Menü-Integration**: Stylesets werden automatisch ins Kontext-Menü hinzugefügt (rechtsklick), sofern aktive Stylesets vorhanden sind.
12+
* **Intelligente Gruppierung**: Formate werden nach ihrer Profil-Zuordnung sortiert und gruppiert, ohne die Filterung nach `selector`/`block`/`inline` zu beeinträchtigen.
13+
414
Version 8.9.1
515
-------------------------------
616

assets/scripts/base.js

Lines changed: 155 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,68 @@ $(document).on('click', '[data-yform-be-relation-moveup], [data-yform-be-relatio
366366
}, 100);
367367
});
368368

369+
/**
370+
* Groups style formats by their profile assignments and styleset.
371+
* Adds separators with profile+styleset labels between different groups.
372+
*
373+
* @param {Array} formatsWithProfiles - Array with {format: {...}, profiles: [...], styleset: "..."}
374+
* @returns {Array} - Grouped formats with separators
375+
*/
376+
function groupFormatsByProfiles(formatsWithProfiles) {
377+
if (!Array.isArray(formatsWithProfiles) || formatsWithProfiles.length === 0) {
378+
return [];
379+
}
380+
381+
// Group by profile string + styleset (empty profiles = all profiles)
382+
let groupedByKey = {};
383+
384+
formatsWithProfiles.forEach(function(item) {
385+
if (!item.format) return;
386+
387+
// Convert profiles array to a key (sort for consistent grouping)
388+
let profileKey = '';
389+
if (item.profiles && item.profiles.length > 0) {
390+
profileKey = item.profiles.sort().join(', ');
391+
}
392+
393+
let styleset = item.styleset || 'Default';
394+
let groupKey = styleset + '|' + profileKey;
395+
396+
if (!groupedByKey[groupKey]) {
397+
groupedByKey[groupKey] = {
398+
profileKey: profileKey,
399+
styleset: styleset,
400+
formats: []
401+
};
402+
}
403+
groupedByKey[groupKey].formats.push(item.format);
404+
});
405+
406+
// Convert to ordered array with separators
407+
let result = [];
408+
let groupKeys = Object.keys(groupedByKey).sort();
409+
410+
groupKeys.forEach(function(key, index) {
411+
let group = groupedByKey[key];
412+
413+
// Add separator with profile label (except for first group)
414+
if (index > 0) {
415+
let profileLabel = group.profileKey === '' ? 'All Profiles' : group.profileKey;
416+
let label = group.styleset + ' (' + profileLabel + ')';
417+
result.push({
418+
title: '─ ' + label + ' ─',
419+
isDisabled: true,
420+
onSelect: function() { return false; }
421+
});
422+
}
423+
424+
// Add formats from this group
425+
result = result.concat(group.formats);
426+
});
427+
428+
return result;
429+
}
430+
369431
function tiny_init(container) {
370432
let profiles = {};
371433

@@ -466,14 +528,79 @@ function tiny_init(container) {
466528
// This ensures style_formats are available when TinyMCE registers the 'styles' button
467529
if (typeof rex !== 'undefined' && rex.tinyGlobalOptions) {
468530
let globalOpts = rex.tinyGlobalOptions;
531+
let profileNamesById = (typeof rex.tinyProfileNamesById === 'object' && rex.tinyProfileNamesById) ? rex.tinyProfileNamesById : {};
532+
533+
function normalizeProfileName(value) {
534+
if (typeof value !== 'string') {
535+
return '';
536+
}
537+
return value.trim().toLowerCase();
538+
}
539+
540+
function getNormalizedProfileCandidates(profileValue) {
541+
let candidates = [];
542+
let profileRaw = (profileValue === undefined || profileValue === null) ? '' : String(profileValue).trim();
543+
let normalizedProfile = normalizeProfileName(profileRaw);
544+
if (normalizedProfile !== '') {
545+
candidates.push(normalizedProfile);
546+
}
547+
548+
if (profileRaw !== '' && Object.prototype.hasOwnProperty.call(profileNamesById, profileRaw)) {
549+
let mappedName = normalizeProfileName(String(profileNamesById[profileRaw]));
550+
if (mappedName !== '' && candidates.indexOf(mappedName) === -1) {
551+
candidates.push(mappedName);
552+
}
553+
}
554+
555+
return candidates;
556+
}
557+
558+
let profileCandidates = getNormalizedProfileCandidates(profile);
559+
560+
function normalizeStylesetsInToolbar(toolbarValue) {
561+
if (typeof toolbarValue === 'string') {
562+
return toolbarValue.replace(/\bstyles\b/g, 'stylesets');
563+
}
564+
565+
if (Array.isArray(toolbarValue)) {
566+
return toolbarValue.map(function(row) {
567+
if (typeof row !== 'string') {
568+
return row;
569+
}
570+
return row.replace(/\bstyles\b/g, 'stylesets');
571+
});
572+
}
573+
574+
return toolbarValue;
575+
}
469576

470577
// Helper function to check if a Style-Set applies to this profile
471578
// Empty profiles array means it applies to ALL profiles
472579
function appliesToProfile(profilesList) {
473580
if (!profilesList || profilesList.length === 0) {
474581
return true; // Empty = applies to all profiles
475582
}
476-
return profilesList.indexOf(profile) !== -1;
583+
584+
for (let i = 0; i < profilesList.length; i++) {
585+
let profileEntry = String(profilesList[i] || '').trim();
586+
if (profileEntry === '') {
587+
continue;
588+
}
589+
590+
let normalizedEntry = normalizeProfileName(profileEntry);
591+
if (profileCandidates.indexOf(normalizedEntry) !== -1) {
592+
return true;
593+
}
594+
595+
if (Object.prototype.hasOwnProperty.call(profileNamesById, profileEntry)) {
596+
let mappedEntry = normalizeProfileName(String(profileNamesById[profileEntry]));
597+
if (profileCandidates.indexOf(mappedEntry) !== -1) {
598+
return true;
599+
}
600+
}
601+
}
602+
603+
return false;
477604
}
478605

479606
// Merge content_css (array) - filter by profile
@@ -514,39 +641,40 @@ function tiny_init(container) {
514641
}
515642
}
516643

517-
// Merge style_formats - filter by profile
644+
// Merge style_formats - filter by profile, group by profile with separators
518645
// New format: [{format: {...}, profiles: ["uikit"]}]
519-
// Legacy format: [{title: "...", items: [...]}]
646+
// Format can be a group: {title: "...", items: [...]}
520647
if (globalOpts.style_formats && globalOpts.style_formats.length > 0) {
521-
let filteredFormats = [];
648+
let formatsWithProfiles = [];
649+
let legacyFormats = [];
650+
522651
globalOpts.style_formats.forEach(function(item) {
523652
if (item.format && appliesToProfile(item.profiles)) {
524-
// New format with profile filter
525-
filteredFormats.push(item.format);
653+
// New format with profile info - keep for grouping
654+
formatsWithProfiles.push(item);
526655
} else if (item.title) {
527656
// Legacy format (has title = is a format group) - always include
528-
filteredFormats.push(item);
657+
legacyFormats.push(item);
529658
}
530659
});
531660

532-
if (filteredFormats.length > 0) {
661+
if (formatsWithProfiles.length > 0 || legacyFormats.length > 0) {
533662
// Enable merging with default formats (Headings, Inline, Blocks, Align)
534663
options.style_formats_merge = true;
535664

536665
if (!options.style_formats) {
537666
options.style_formats = [];
538667
}
539-
// Append filtered style formats to existing ones
540-
options.style_formats = options.style_formats.concat(filteredFormats);
541668

542-
// Replace 'styles' with 'stylesets' in toolbar (our custom button)
543-
if (options.toolbar && typeof options.toolbar === 'string') {
544-
// Replace existing 'styles' with 'stylesets'
545-
options.toolbar = options.toolbar.replace(/\bstyles\b/g, 'stylesets');
546-
// If neither exists, add stylesets at the beginning
547-
if (options.toolbar.indexOf('stylesets') === -1) {
548-
options.toolbar = 'stylesets ' + options.toolbar;
549-
}
669+
// Group new formats by profile and add separators
670+
let groupedFormats = groupFormatsByProfiles(formatsWithProfiles);
671+
672+
// Append grouped formats first, then legacy formats
673+
options.style_formats = options.style_formats.concat(groupedFormats).concat(legacyFormats);
674+
675+
// Keep user-defined toolbar order; only normalize legacy 'styles' -> 'stylesets'.
676+
if (options.toolbar) {
677+
options.toolbar = normalizeStylesetsInToolbar(options.toolbar);
550678
}
551679

552680
// Add stylesets to Format menu
@@ -558,6 +686,15 @@ function tiny_init(container) {
558686
items: 'bold italic underline strikethrough superscript subscript codeformat | stylesets blocks fontfamily fontsize align lineheight | forecolor backcolor | removeformat'
559687
};
560688

689+
// Add stylesets to context menu if available
690+
if (!options.contextmenu) {
691+
options.contextmenu = 'link image table';
692+
}
693+
// Add stylesets to existing contextmenu if not already there
694+
if (typeof options.contextmenu === 'string' && options.contextmenu.indexOf('stylesets') === -1) {
695+
options.contextmenu = options.contextmenu + ' | stylesets';
696+
}
697+
561698
}
562699
}
563700
}

assets/scripts/profile_builder.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function initTinyMceProfileAssistant() {
4949
const options = rex.tinymceProfileOptions || {};
5050
// Sort plugin and toolbar lists alphabetically for easier scanning in the assistant.
5151
const pluginsList = (options.plugins || []).slice().sort((a, b) => String(a).localeCompare(String(b)));
52-
const toolbarButtons = (options.toolbar || []).slice().sort((a, b) => String(a).localeCompare(String(b)));
52+
const toolbarButtons = Array.from(new Set((options.toolbar || []).concat(['stylesets']))).sort((a, b) => String(a).localeCompare(String(b)));
5353

5454
// Plugins Section – FOR plugins are highlighted as FriendsOfREDAXO custom plugins.
5555
// Besides the `for_*` naming convention there are legacy custom plugins that predate
@@ -64,7 +64,12 @@ function initTinyMceProfileAssistant() {
6464
const isAddonPlugin = (name) => addonPluginSet.has(name);
6565
const isAddonToolbarBtn = (name) => addonToolbarSet.has(name);
6666
const isForPlugin = (name) => typeof name === 'string' && (name.indexOf('for_') === 0 || forPluginSet.has(name));
67-
const isForToolbarBtn = (name) => typeof name === 'string' && (name.indexOf('for_') === 0 || forToolbarSet.has(name));
67+
const isForToolbarBtn = (name) => {
68+
if (typeof name !== 'string') {
69+
return false;
70+
}
71+
return name === 'stylesets' || name.indexOf('for_') === 0 || forToolbarSet.has(name);
72+
};
6873
let pluginsHtml = '<legend>' + (i18n.plugins || 'Plugins') + ' '
6974
+ '<small class="for-plugin-legend-hint"><span class="for-plugin-badge-inline">FOR</span> ' + (i18n.for_plugins_hint || 'FriendsOfREDAXO custom plugins') + '</small> '
7075
+ '<small class="for-plugin-legend-hint"><span class="for-plugin-badge-inline for-plugin-badge--addon">AddOn</span> ' + (i18n.addon_plugins_hint || 'Plugins aus externen AddOns') + '</small>'
@@ -2112,6 +2117,9 @@ function normalizeToolbarItemValue(value) {
21122117
if (normalized === '|' || normalized.toLowerCase() === 'separator') {
21132118
return '|';
21142119
}
2120+
if (normalized.toLowerCase() === 'styles') {
2121+
return 'stylesets';
2122+
}
21152123
return normalized;
21162124
}
21172125

assets/scripts/profiles-list.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,13 @@
119119
if (!url) return;
120120

121121
var nameEl = document.getElementById('tinymcePreviewName');
122+
var descriptionEl = document.getElementById('tinymcePreviewDescription');
122123
var jsonEl = document.getElementById('tinymcePreviewJson');
123124

124125
var loadingText = findLoadingText();
125126

126127
if (nameEl) nameEl.textContent = '';
128+
if (descriptionEl) descriptionEl.textContent = '';
127129
if (jsonEl) jsonEl.textContent = loadingText;
128130

129131
// show modal (bootstrap)
@@ -139,12 +141,15 @@
139141
})
140142
.then(function (data) {
141143
var profileName = data.name || 'full';
144+
var profileDescription = data.description || '';
142145
var raw = data.extra || '';
143146

144147
var modalBody = document.querySelector('#tinymcePreviewModal .modal-body');
145148
if (modalBody) {
146149
// create editor element using existing init system via data-profile
150+
var descriptionHtml = profileDescription ? '<p style="margin:0 0 12px 0; color:#666;">' + escapeHtml(profileDescription) + '</p>' : '';
147151
modalBody.innerHTML = '<h4 style="margin-top:0">' + escapeHtml(profileName) + '</h4>'
152+
+ descriptionHtml
148153
+ '<textarea id="tinymcePreviewEditor" class="tiny-editor" data-profile="' + escapeHtml(profileName) + '">The quick brown fox jumps over the lazy dog.</textarea>';
149154
}
150155

0 commit comments

Comments
 (0)