@@ -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 ( / \b s t y l e s \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