Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize the Froala default toolbar #2078

Closed
gabsource opened this issue May 31, 2016 · 15 comments
Closed

Customize the Froala default toolbar #2078

gabsource opened this issue May 31, 2016 · 15 comments

Comments

@gabsource
Copy link
Contributor

Expected behavior

With the previous editor we could somehow extend the JS code inside a plugin to add buttons to the rich editor toolbar.

Actual behavior

We should be abale to customize the Froala editor toolbar with custom buttons (not only the one available in the bundled Froala version) :

  • default buttons added to all toolbar
  • specific buttons for a single editor form (via toolbarButtons of the Yaml)

Buttons defined like in https://www.froala.com/wysiwyg-editor/docs/concepts/custom-button must be registered before the editor is instantiated but it seems we can not do this at the moment.

Reproduce steps

Not relevant

October build

341

@daftspunk
Copy link
Member

daftspunk commented May 31, 2016

  • specific buttons for a single editor form (via toolbarButtons of the Yaml)

Above has always been possible

@daftspunk
Copy link
Member

The default rich editor buttons can now be customized by modifying the $.oc.richEditorButtons object.

@gabsource
Copy link
Contributor Author

Great ! We can use it like in the code below but I think there can be a race problem that depends on when the custom extension JS is laoded.

In this order :

  1. plugin/extension.js
  2. richeditor/build-min.js

We must hook the render event to be able to override the buttons array or even create the Froala plugins

In this order :

  1. richeditor/build-min.js
  2. plugin/extension.js

We must directly extend Froala and add the button in the main JS flow as the render event would be called after the RichEditor one.

Maybe throwing an event in RichEditor.prototype.init before the initFroala would allow plugins to dynamically interact with Froala in a safe way (add functions, buttons to toolbar...) ?

This illustrate a case when the custom extension is loaded before the richeditor/build-min.js.

hook in Plugin boot

class Plugin extends PluginBase {
    public function boot()
    {
        \Event::listen('backend.form.extendFields', function ($form) {
            foreach ($form->getFields() as $field) {
                if ( ! empty( $field->config['type'] ) && str_contains($field->config['type'], 'richeditor')) {
                    $form->addJs('/plugins/acme/plugin/assets/js/froala.plugins.js', 'Acme.Plugin');
                    break;
                }
            }
        });
    }
}

froala.plugins.js

+function ($) {
    var Plugins = {
        init: function () {
            $.FroalaEditor.DefineIcon('buttonIcon', {NAME: 'star'})
            $.FroalaEditor.RegisterCommand('myButton', {
                title: 'My Button',
                icon: 'buttonIcon',
                undo: true,
                focus: true,
                refreshAfterCallback: true,
                callback: function () {
                    console.log(this.html.get());
                },
                refresh: function ($btn) {
                    console.log(this.selection.element());
                }
            })

            $.oc.richEditorButtons.push('myButton')
        }
    }

    $(document).on('render', function () {
        Plugins.init()
    })
}(jQuery)

@daftspunk
Copy link
Member

I feel like this could be better. The logic is really coupled to the RichEditor widget class, not really the Form widget class. We should add behaviors to the WidgetBase for extensibility anyway (see #1586).

RichEditor::extend(function($widget) {
    $widget->addJs(...);
});

This would resolve the race condition and make everything hunky-dory again.

@seanthepottingshed
Copy link

@gabsource Did you have any joy extending the toolbar as per your example above? I'm trying but not getting very far... Any insight would be gratefully appreciated ;)

@Harti
Copy link
Contributor

Harti commented Jun 21, 2017

Gotta agree with @seanthepottingshed. It has been a try-and-error pain for me and I haven't gotten it to work so far. Granted, I'm fairly new to OctoberCMS and how its plugin extension mechanisms work but I've really sat here for a whole day but it just didn't work.

@daftspunk
Is there a go-to example for this, and do you really think that the editor buttons shouldn't be customizable via a config file?

@seanthepottingshed
Copy link

seanthepottingshed commented Jun 21, 2017

@Harti

Here is an example for a SoundCloud embed I wrote:

froala-soundcloud-plugin.js

/*
|-------------------------
| Froala SoundCloud Plugin
|-------------------------
*/
+function ($) {
    /*
    |--------------------------
    | Initialise SoundCloud SDK
    |--------------------------
     */
    SC.initialize({
        client_id : '{{ clientID }}'
    });
    /*
    |----------------------------------------------------
    | Define SoundCloud Icon, Register Command and Button
    |----------------------------------------------------
     */
    $.FroalaEditor.DefineIcon('soundCloudIcon', {
        NAME : 'soundcloud'
    });
    $.FroalaEditor.RegisterCommand('insertSoundCloudTrack', {
        title                : 'Insert SoundCloud Track',
        icon                 : 'soundCloudIcon',
        showOnMobile         : true,
        refreshAfterCallback : false,
        undo                 : false,
        focus                : false,
        callback             : function() {
            this.soundCloudPlugin.showPopup();
        }
    });
    /*
    |----------------------------------------
    | Register Insert SoundCloud HTML Command
    |----------------------------------------
     */
    $.FroalaEditor.RegisterCommand('insertSoundCloudHTML', {
        undo                 : true,
        refreshAfterCallback : false,
        callback             : function() {
            var editor         = this,
                $trackURLField = $('#soundcloud-popup input'),
                $header        = $('#soundcloud-popup .fr-buttons'),
                trackURL       = $trackURLField.val();
            $header.addClass('testing');
            $header.removeClass('invalid');
            SC.get('/resolve/?url=' + encodeURIComponent(trackURL), {
                limit : 1
            }, function(data) {
                if(data.kind && data.kind === 'track') {
                    var trackTitle     = data.user.username + ' | ' + data.title,
                        vimeoVideoHTML = '<figure class="tps" data-ui-block="false" contenteditable="false" tabindex="0">' + "\n" +
                        '<blockquote class="placeholder soundcloud-track">' + "\n" +
                        '<a href="' + trackURL + '" target="_blank">' + "\n" +
                        trackTitle +
                        '</a>' + "\n" +
                        '</blockquote>' + "\n" +
                        '<iframe width="100%" height="450" scrolling="no" frameborder="no" data-src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' +
                        data.id +
                        '&auto_play=false&hide_related=true&show_comments=true&show_user=false&show_reposts=false&visual=false;&color=' +
                        accentColor.replace('#', '') +
                        '">' + "\n" +
                        '</iframe>' + "\n" +
                        '</figure>' + "\n";
                    editor.html.insert(vimeoVideoHTML);
                    editor.undo.saveStep();
                    editor.soundCloudPlugin.hidePopup();
                    $trackURLField.val('');
                    $header.removeClass('testing');
                } else {
                    $header.removeClass('testing');
                    $header.addClass('invalid');
                }
            });
        } 
    });
    $.oc.richEditorButtons.push('insertSoundCloud');
    /*
    |-------------------------------
    | Define SoundCloud Plugin Popup
    |-------------------------------
    */
    $.extend($.FroalaEditor.POPUP_TEMPLATES, {
      'soundCloudPlugin.popup': '[_CUSTOM_LAYER_]'
    });
    $.FroalaEditor.PLUGINS.soundCloudPlugin = function (editor) {
        function initPopup() {
            var customLayer = '<div id="soundcloud-popup" class="tps-popup">' + "\n" +
                              '<div class="fr-buttons">' + "\n" +
                              '<label for="">SoundCloud</label>' + "\n" +
                              '<div class="loading-indicator">' + "\n" +
                              '<span></span>' + "\n" +
                              '</div>' + "\n" +
                              '<p class="error-message">Invalid URL</p>' + "\n" +
                              '</div>' + "\n" +
                              '<div class="form">' + "\n" +
                              '<input placeholder="Paste Embed URL">' + "\n" + 
                              '<div class="fr-action-buttons">' + "\n" +
                              '<button type="button" class="fr-command fr-submit" data-cmd="insertSoundCloudHTML" tabindex="2" role="button">Insert SoundCloud</button>' + "\n" +
                              '</div>' + "\n" +
                              '</div>' + "\n" +
                              '</div>',
                template = {
                    custom_layer : customLayer 
                },
                $popup = editor.popups.create('soundCloudPlugin.popup', template);     
            return $popup;
        }
        /**
         * Show Popup.  
         */
        function showPopup() {
            var $popup = editor.popups.get('soundCloudPlugin.popup');
            if (!$popup) {
                $popup = initPopup();
            }
            editor.popups.setContainer('soundCloudPlugin.popup', editor.$tb);
            var $btn = editor.$tb.find('.fr-command[data-cmd="insertSoundCloudTrack"]'),
                left = $btn.offset().left + $btn.outerWidth() / 2,
                top  = $btn.offset().top + (editor.opts.toolbarBottom ? 10 : $btn.outerHeight() - 10);
            editor.popups.show('soundCloudPlugin.popup', left, top, $btn.outerHeight());
        }
        /**
         * Hide Popup.
         */
        function hidePopup () {
          editor.popups.hide('soundCloudPlugin.popup');
        }
        return {
            showPopup : showPopup,
            hidePopup : hidePopup
        };
    };
}(jQuery);

froala-soundcloud-plugin.less

/*
|-------------------------
| Froala SoundCloud Plugin
|-------------------------
 */
.fr-popup {
  .tps-popup {
    .fr-buttons {
      position: relative;
      height: 35px;
      font-size: 14px;
      font-weight: 100;
      font-family: 'Roboto';
      line-height: 34px;
      label {
        display: block;
        float: left;
        margin: 0;
        padding: 0 0 0 10px;
      }
      .loading-indicator {
        position: absolute;
        right: 10px;
        padding: 0;
        width: 34px;
        height: 34px;
        text-align: right;
        background: none;
        span {
          opacity: 0;
          background-size: 20px 20px;
          transition: opacity 0.125s linear;
        }
      }
      .error-message {
        float: right;
        margin: 0;
        padding: 0 10px 0 0;
        color: @accentColor;
        font-weight: 700;
        opacity: 0;
        transition: opacity 0.125s linear;
      }
      &.testing {
        .loading-indicator {
          span {
            opacity: 1;
          }
        }
        .error-message {
          opacity: 0;
        }
      }
      &.invalid {
        .error-message {
          opacity: 1;
        }
      }
    }
    .form {
      padding: 10px;
      input {
        margin: 0;
        padding: 8px 13px 9px;
        border: 1px solid #d1d6d9;
        height: 38px;
        line-height: 1.42857143;
        font-size: 14px;
        font-family: 'Roboto';
        border-radius: 3px;
        box-shadow: inset 0 1px 0 rgba(209,214,217,0.25), 0 1px 0 rgba(255,255,255,.5);
      }
      .fr-action-buttons {
        button {
          &.fr-command {
            width: 100%;
            font-family: 'Roboto';
            text-shadow: none;
            text-align: center;
            background-color: @primaryColor;
            &:hover {
              background-color: @secondaryColor;
              transition: background-color 0.25s linear;
            }
          }
        }
      }
    }
  }
}
.control-richeditor {
  figure[data-ui-block] {
    &.tps {
      position: relative;
      margin: 15px 0;
      padding: 0;
      border: 2px solid #FFFFFF;
      &:focus {
        border: 2px solid @primaryColor;
      }
      .video-player {
        position: relative;
        display: block;
        width: 100%;
        height: 0;
        padding: 0 0 56.25% 0;
        iframe {
          position: absolute;
          z-index: 1;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background: #000;
        }
      }
      /*
      |-------------------
      | Editor Placeholder
      |-------------------
      */
      .placeholder {
        margin: 0;
        padding: 0 15px;
        width: 100%;
        background: #f2f2f2;
        border: none;
        border-radius: 3px;
        box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 1px 1px rgba(0,0,0,0.16);
        a {
          position: relative;
          display: block;
          margin: 0 0 0 40px;
          line-height: 50px;
          color: rgba(64,82,97,0.8) !important;
          font-size: 18px;
          font-weight: 700;
          font-family: 'Roboto';
          text-decoration: none;
          &:hover {
            color: @accentColor !important;
            transition: color 0.25s linear;
          }
          &:before {
            position: absolute;
            top: 0;
            left: -40px;
            font-family: 'FontAwesome';
            font-size: 30px;
            font-weight: 100;
          }
        }
        &.soundcloud-track {
          a {
            &:before {
              content: "\f1be";
              color: #ff7700;
            }
          }
          + iframe {
            position: absolute;
            height: 0;
          }
        }
      }
    }
  }
}

LukeTowers pushed a commit that referenced this issue Jul 4, 2017
Added support for changing the global default for richeditor buttons. 
Addresses: #2677, #2384, #2078, #1743 and rainlab/pages-plugin#188
@mrelevance
Copy link

This approach doesn't seem to work anymore as of build 455. Is there a different way that should be used to would work to add buttons to the Rich Editor from a plugin?

@LukeTowers
Copy link
Contributor

@mrelevance what doesn't work about it? Are you getting a specific error message?

@mrelevance
Copy link

It simply just doesn't show up in the toolbar.

I add the JS plugin file like this:

        RichEditor::extend(function ($widget) {
            $widget->addJs($this->pluginsPath.'/mrelevance/mediablocks/widgets/ramediamanager/assets/js/raMediaManager.froala.js');
        });

In build 447 it adds the button like so:
Screenshot_20190521_101840

But, in build 455 it doesn't show at all anymore.
Screenshot_20190521_102019

Here is a small snippet of how I am adding the button:

$.FE.DEFAULTS.imageInsertButtons.push('raImageManager');

  $.FroalaEditor.RegisterCommand('raImageManager', {
    title: 'Browse Media',
    // icon: 'star',
    undo: false,
    focus: false,
    callback: function () {
      this.raMediaManager.insertImage();
    },
    plugin: 'raMediaManager'
  });

  $.FE.DefineIcon('raImageManager', {
    NAME: 'star',
  });

@LukeTowers
Copy link
Contributor

@mrelevance have you checked the dev console? Are there any errors being displayed?

@mrelevance
Copy link

@LukeTowers no errors are reported in the console.

@mrelevance
Copy link

@LukeTowers bumping as I haven't gotten any response to the last message, any ideas?

@LukeTowers
Copy link
Contributor

@mrelevance this was fixed in 30f4d4c

@daftspunk
Copy link
Member

For anyone stumbling across this recently, this has been addressed as an API in v3.4. The documentation now includes these instructions:


Registering a Custom Button

The following JavaScript code can be used to register a custom button as a command.

oc.richEditorRegisterButton('insertCustomThing', {
    title: 'Insert Something',
    icon: '<i class="icon-star"></i>',
    undo: true,
    focus: true,
    refreshOnCallback: true,
    callback: function () {
        this.html.insert('<strong>My Custom Thing!</strong>');
    }
});

Then add the button to the default collection.

oc.richEditorButtons.splice(0, 0, 'insertCustomThing');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

6 participants