XF 2.1 Modifying Froala Editor options?

Will Watts

Active member
Hey all,

We're looking for a way to add some tweaks to the Froala Editor settings - in particular we'd like to change the buttons that remain active when the editor is in codeview mode, using this option here:

What's the best way for us to extend this through XF? Is it extendable through the normal class system as with PHP code? Are there specific hooks we can use to make the modification?

Thanks for any help on this!
 

Mike

XenForo developer
Staff member
We don't actually expose the code view you're referring to -- the BB code toggle is a totally different and custom system, so what you're trying to do likely won't work.

If you want to adjust the editor configuration though, you need to do this via JS. We fire a editor:config event. You can listen to this (at the document level) and modify the configuration that it receives as you need.
 

niemiro

New member
We don't actually expose the code view you're referring to -- the BB code toggle is a totally different and custom system, so what you're trying to do likely won't work.

If you want to adjust the editor configuration though, you need to do this via JS. We fire a editor:config event. You can listen to this (at the document level) and modify the configuration that it receives as you need.

Hi Mike,

I'm another admin from the same website as Will Watts (the thread starter here).

I see what you mean now about being the BBCode editor being a totally different system.

In the end we took a different approach - we leave the WYSIWYG editor textbox viewable all the time (so the editor buttons always work), and the bbcode textbox hidden all the time, and when you toggle into BBCode mode, it simply converts all current HTML into BBCode, and vice-versa when toggling back out. Then you can carry on editing the BBCodes either manually or in WYSIWYG mode.

You always start in WYSIWYG mode.
You may then type out the word "Test" and use the editor buttons to make it bold: Test
Click the BBCode button to move in to BBCode mode: [B]Test[/B]
Use the editor buttons to add a strikethrough effect: [B]Test[/B]
Either post your thread or return from BBCode Mode: Test
Click the BBCode Mode button again: [S][B]Test[/B][/S]

I really believe you should consider taking this same approach inside the original XenForo.

It offers the big advantage of being able to continue to use the editor buttons at all times, and in no way stops people from manually typing / tweaking the BBCodes if they so wish. I really feel that this way is simply better, and you should consider it.

All the best,

Richard


[For anyone else wanting the same, we add the following code into a new file (xf/js/editor_somename.js), and then call that extra file from the top of the editor template).]
PHP:
!function($, window, document, _undefined)
{
    "use strict";

    XF.Sysnative = {
        init: function(e, ed, xfEditor)
        {
            var _isBbCodeView = false;
           
            function getButton()
            {
                return ed.$tb.find('.fr-command[data-cmd=xfBbCode]');
            }
           
            function getBbCodeBox()
            {
                var $oel = ed.$oel;

                var $bbCodeBox = $oel.data('xfBbCodeBox');
                if (!$bbCodeBox)
                {
                    var borderAdjust = parseInt(ed.$wp.css('border-bottom-width'), 10)
                        + parseInt(ed.$wp.css('border-top-width'), 10);

                    $bbCodeBox = $('<textarea class="input" style="display: none" />');
                    $bbCodeBox.attr('aria-label', XF.htmlspecialchars(XF.phrase('rich_text_box')));
                    $bbCodeBox.css({
                        minHeight: ed.opts.heightMin ? (ed.opts.heightMin + borderAdjust) + 'px' : null,
                        maxHeight: ed.opts.heightMax ? ed.opts.heightMax + 'px' : null,
                        height: ed.opts.height ? (ed.opts.height + borderAdjust) + 'px' : null,
                        padding: ed.$el.css('padding')
                    });
                    $bbCodeBox.attr('name', $oel.data('original-name'));
                    $oel.data('xfBbCodeBox', $bbCodeBox);
                    ed.$wp.after($bbCodeBox);

                    XF.Element.applyHandler($bbCodeBox, 'textarea-handler');
                    XF.Element.applyHandler($bbCodeBox, 'user-mentioner');
                    XF.Element.applyHandler($bbCodeBox, 'emoji-completer');
                }

                return $bbCodeBox;
            }
           
       
            ed.bbCode.toBbCode = function toBbCode(bbCode, skipFocus)
            {
                var apply = function(bbCode, skipFocus)
                {
                    _isBbCodeView = true;
                   
                    getButton().css('color', '#F2930D');
                    getBbCodeBox().prop('disabled', true);

                    ed.undo.saveStep();
                    ed.html.set(bbCode.replace(/(?:\r\n|\r|\n)/g, '<br>'));
                };

                if (typeof bbCode == 'string')
                {
                    apply(bbCode, skipFocus);
                }
                else
                {
                    XF.ajax('POST',
                        XF.canonicalizeUrl('index.php?editor/to-bb-code'),
                        { html: ed.html.get() },
                        function (data) { apply(data.bbCode, skipFocus); }
                    );
                }
            };
           
           
            ed.bbCode.toHtml = function(bbCode, skipFocus)
            {
                var apply = function(html)
                {
                    _isBbCodeView = false;
                    getButton().removeAttr('style');

                    ed.html.set(html);
                    ed.undo.saveStep();
                };

                if (typeof html == 'string')
                {
                    apply(html);
                }
                else
                {
                    XF.ajax('POST',
                        XF.canonicalizeUrl('index.php?editor/to-bb-code'),
                        { html: ed.html.get() },
                        function (data) {
                            var params = { bb_code: data.bbCode };
                           
                            var $form = ed.$el.closest('form');
                            if ($form.length)
                            {
                                if ($form[0][ed.opts.xfBbCodeAttachmentContextInput])
                                {
                                    params.attachment_hash_combined = $($form[0][ed.opts.xfBbCodeAttachmentContextInput]).val();
                                }
                            }
       
                            XF.ajax('POST',
                                XF.canonicalizeUrl('index.php?editor/to-html'),
                                params,
                                function (data) { apply(data.editorHtml); }
                            );
                        }
                    );
                }
            };
           
            ed.bbCode.toggle = function()
            {
                if (_isBbCodeView)
                {
                    this.toHtml();
                }
                else
                {
                    this.toBbCode();
                }
            };
           
            ed.bbCode.getTextArea = function()
            {
                return null;
            }
       
            ed.$wp.css('display', '');
            getBbCodeBox().css('display', 'none').prop('disabled', true);
            ed.$tb.find(' > .fr-command').not(getButton().removeClass('fr-active')).removeClass('fr-disabled');
            ed.$oel.prop('disabled', false);
            XF.setIsEditorEnabled(true);
            XF.layoutChange();
        }
    };

    $(document).on('editor:init', XF.Sysnative.init);
}
(jQuery, window, document);
 

niemiro

New member
BTW if anyone wants a true BBCode mode, you can insert this code underneath XF.layoutChange()

PHP:
            ed.events.on('commands.after', function(cmd)
            {
                if (_isBbCodeView)
                {
                    var editorHtml = ed.html.get();
               
                    switch(cmd)
                    {
                        case 'bold':
                            editorHtml = editorHtml.replace("<strong>", "[b]").replace("</strong>", "[/b]");
                            break;
                    }
               
                    ed.html.set(editorHtml);
                }
            });

I'm not sure if we're actually going to use this on our website though. If we do we'll post an updated version later, if not you're free to build upon this incomplete and untested implementation. (The replaces probably need converting to use global /<strong>/g for starters.)

Either way @Mike and @Chris D I think it would take much less work than you might immediately think to implement a proper BBCode editor. I also don't see any real benefit to the double text box configuration (even though I know vBulletin used to do it that way). Since the HTML editor actually supports BBCode, I think you should use Froala for everything to enable the editor buttons, and then put in a few lines of code to make the editor buttons work properly by changing HTML --> BBCode. I think it would be great for XenForo and not very much developer time.

EDIT: Updated version
PHP:
            ed.events.on('commands.after', function(cmd)
            {
                if (_isBbCodeView)
                {
                    var editorHtml = ed.html.get();
                   
                    // Already displayed as BBCode (no processing needed): xfMedia, xfQuote, xfCode, xfInlineCode, xfSpoiler, xfInlineSpoiler
                    switch(cmd)
                    {
                        case 'bold':
                            editorHtml = editorHtml.replace(/<strong>/g, "[B]").replace(/<\/strong>/g, "[/B]");
                            break;
                       
                        case 'italic':
                            editorHtml = editorHtml.replace(/<em>/g, "[I]").replace(/<\/em>/g, "[/I]");
                            break;
                           
                        case 'underline':
                            editorHtml = editorHtml.replace(/<u>/g, "[U]").replace(/<\/u>/g, "[/U]");
                            break;
                       
                        case 'strikeThrough':
                            editorHtml = editorHtml.replace(/<s>/g, "[S]").replace(/<\/s>/g, "[/S]");
                            break;
                       
                        case 'textColor':
                            // editorHtml = editorHtml.replace(/<span style="color: (.+?);">/g, "[COLOR=$1]").replace(/<\/span>/g, "[/COLOR]"); // Doesn't work.
                            break;
                       
                        case 'fontFamily':
                            editorHtml = editorHtml.replace(/<span style="font-family: '(.+?)';">/g, "[FONT=$1]").replace(/<\/span>/g, "[/FONT]");
                            break;
                       
                        case 'fontSize':
                            editorHtml = editorHtml.replace(/<span style="font-size: (\d+?)px;">/g, "[SIZE=$1px]").replace(/<\/span>/g, "[/SIZE]"); // Ideally, convert px to size numbers properly.
                            break;
                       
                        case 'linkInsert':
                            // editorHtml = editorHtml.replace(/<a href="(.+?)" .+?>(.+?)<\/a>/g, "[URL=$1]$2[/URL]"); // Gets inserted at end of post.
                            break;
                       
                        case 'align':
                            editorHtml = editorHtml.replace(/<p style="text-align: (left|center|right);">/g, "[ALIGN=$1]").replace(/<\/p>/g, "[/ALIGN]");
                            break;
                       
                        case 'formatOL': // Ordered list
                            editorHtml = editorHtml.replace(/<ol>/g, "[LIST=1]").replace(/<\/ol>/g, "[/LIST]").replace(/<li( data-xf-list-type="ol")?>(.*?)<\/li>/g, "<br>[*]$2");
                            break;
                       
                        case 'formatUL': // Unordered list
                            editorHtml = editorHtml.replace(/<ul>/g, "[LIST]").replace(/<\/ul>/g, "[/LIST]").replace(/<li( data-xf-list-type="ul")?>(.*?)<\/li>/g, "<br>[*]$2");
                            break;
                       
                        case 'outdent':
                            // Do BBCodes for these even exist? One indent = <p style="margin-left: 20px;">Text</p>
                            break;
                       
                        case 'indent':
                            // Do BBCodes for these even exist? One indent = <p style="margin-left: 20px;">Text</p>
                            break;
                       
                        case 'xfSmilie':
                            // Allow to remain WYSIWYG?
                            break;
                       
                        case 'insertImage':
                            // Allow to remain WYSIWYG?
                            break;
                       
                        case 'insertVideo':
                           
                            break;
                       
                        case 'tableInsert':
                            // Allow to remain WYSIWYG?
                            break;
                           
                        default:
                            // ?? More complex conversions via AJAX call to server?
                            break;
                    }
                   
                    ed.html.set(editorHtml);
                }
            });
 
Last edited:

Slion

Active member
We fire a editor:config event. You can listen to this (at the document level) and modify the configuration that it receives as you need.

Are we sure this is working? By the time I receive the event it looks like the editor has already been created. Also while I was expecting the config as first parameter after the event itself but I actually receive the editor itself from which I can access the opts but changing paragraphFormat does not help then, it is just ignored. Is the jQuery trigger call blocking?
 
Last edited:

Slion

Active member
Are we sure this is working? By the time I receive the event it looks like the editor has already been created. Also while I was expecting the config as first parameter after the event itself but I actually receive the editor itself from which I can access the opts but changing paragraphFormat does not help then, it is just ignored. Is the jQuery trigger call blocking?
My bad, I was using editor:init instead of editor:config, it does work, thanks.
 

Slion

Active member
My bad, I was using editor:init instead of editor:config, it does work, thanks.
In my case adding the flowing to the editor template did the trick:
JavaScript:
<xf:js>
$(document).on('editor:config', function( aEvent, aConfig, aEditor) {
   //console.log(aConfig.paragraphFormat);
   // Instead of just assigning we want to merge our values with our input…
   // …to make sure we are compatible with other addons doing the same.
   // We also need to make sure keys are properly ordered.
   var paraFormat = {}
   var paraFormatOrdered = {
               N: 'Normal',
               H1: 'Heading 1',
               H2: 'Heading 2',
               H3: 'Heading 3',
               H4: 'Heading 4',
               H5: 'Heading 5',
               H6: 'Heading 6'
            }
   // Init our output object to have proper key order
   Object.assign(paraFormat,paraFormatOrdered);
   // Carry over members from our input
   Object.assign(paraFormat,aConfig.paragraphFormat);
   // Make sure we still have our intended values
   aConfig.paragraphFormat = Object.assign(paraFormat,paraFormatOrdered);
   //console.log(aConfig.paragraphFormat);
});   
</xf:js>
 

Lukas W.

Well-known member
For what is worth, you should outsource it into a js file so it can benefit from browser caching. If you need to pass some data to it, you can adjust the template to provide some json that the script can parse in.
 

Slion

Active member
For what is worth, you should outsource it into a js file so it can benefit from browser caching. If you need to pass some data to it, you can adjust the template to provide some json that the script can parse in.
Thanks for the tip. Actually caching is the reason why I put the script inline. For some reason it would not reload after updating the file on the server. Also I was not sure how to package the JS in the add-on. None of those issues with template modifications.
 

Lukas W.

Well-known member
Thanks for the tip. Actually caching is the reason why I put the script inline. For some reason it would not reload after updating the file on the server. Also I was not sure how to package the JS in the add-on. None of those issues with template modifications.
If you're in development mode, you can put the file into src/addons/My/Addon/_files/js/my/addon/file.js and then add addon="My/Addon to the <xf:js/> tag to automatically load the file directly from the dev directory. The build.json in your addon directory will let you define what to bundle up with the add-on on release build.
 
Top