Integrating inline tinymce editor in a component for a gutenberg block

I’m new to stack exchange. I hope my question is not to long.

I want to integrate an inline tinymce editor in a component and use it in a gutenberg block. But my editors ui conflicts with gutenberg. (first screenshot)

The editor-element is inside a backbone view. I can’t use the RichText editor component in my case.

Background:

I‘m developing a plugin to insert leaflet maps into a post https://github.com/jhotadhari/geo-masala

The Plugin has a Gutenberg block to insert and control a map and its appearance/popups/controls/markers… .

Map-Features (Markers/Lines/Polygons) are saved as a custom-post-type: geom_feature. Each geom_feature has meta for geoJSON, appearance…
The custom-post-type is registered without ui. All crud actions for geom_feature(s) are done within a gutenberg block (eg. drawing a marker creates a new geom_feature)

Inside the block, one component renders a leaflet map into a ref element. The component uses backbone to fetch the geom_feature(s) as a collection.
When a Map-Feature (Leaflet.layer) is clicked, a toolbar opens with different editing options. The popup content can be edited within a popup (using a backform form with a custom wysiwyg field). The popup content is the post-content of the geom_feature.

The custom wysiwyg field uses a tinymce inline editor. All (eg updating new content to database) works fine, but the ui of that editor breaks. The editor toolbar and widgets are not in place.

I can init the editor with a fixed_toolbar_container. Like this:

initMceEditor: function(){
    let self = this;

    if ( this.mceEditor )
        return this;

    // setup editor element
    this.getMceElement().attr( 'id', this.cid );
    // setup toolbar element
    this.setupToolbar();

    let settings = _.extend( {}, wp.editor.getDefaultSettings().tinymce, {
        selector: this.cid,
        inline: true,
        toolbar1: [
            'formatselect fontsizeselect bold italic underline',
            'bullist numlist',
            'alignleft aligncenter alignright',
            'link pastetext',
        ].join(' | '),
        toolbar: true,
        fixed_toolbar_container: '#geom-inline-editor-toolbar-' + this.cid,
        content_css: geomData.pluginDirUrl + '/css/tinymce_content.min.css',
        setup: function (editor) {
            // editor events
            // ... some lines skipped
        },
    });

    // init mceEditor
    this.mceEditor = tinymce.createEditor( this.cid, settings );

    // render mceEditor
    this.mceEditor.render();

    return this;
},

This works but the appearance of the toolbar is not as expected. And mce panels and tooltips are displayed at the end of the document. See the first screenshot.

enter image description here

I can apply some style to fix this:

.geom-toolbar {
    display: table;
    width: 100%;
    position: relative;
    .geom-inline-editor-toolbar {
        width: 100%;
        .mce-flow-layout-item {
            float: left;
        }
        .mce-widget.mce-btn {
            float: left;
            button {
                padding: 0;
            }
        }
    }
}

// stupid fix, dont do that!
.mce-widget.mce-tooltip[role="presentation"] {
    position: absolute;
    background-color: #111;
    padding: 0.5em;
}
.mce-container.mce-panel.mce-floatpanel {
    position: absolute;
    max-width: 200px;
    width: 200px !important;
}

The Editor looks like this then:

enter image description here

That’s not a good solution at all. And it will conflict somewhere.

Back to the Question

Does someone knows a way, to include an inline tinymce editor that doesn’t conflict with gutenberg?

Do I have to load some default styles for my tinymce editor that are not loaded by gutenberg? And how to limit their scope to my components
Or can advise me way.

I don’t search for a solution that uses gutenbergs RichText component.

Thank you

1 Answer
1

Gutenbergs FreeformEdit Component (used for the classic editor block) demonstrates the way how to integrate a tinyMce editor.

Recoded my custom Backform WysiwygControl and adopted the tinyMce integration.

/**
 * External dependencies
 */
import Backform from 'Backform';

/**
 * WordPress dependencies
 */
const { __ } = wp.i18n;
const { F10, ESCAPE, ALT } = wp.utils.keycodes;

/**
 * Wysiwyg Control
 *
 * The tinyMce integration is mostly copied from the gutenberg/core-blocks/freeform component (classic editor)
 *
 */
const WysiwygControl = Backform.Control.extend({

    defaults: {
        label: '',
        helpMessage: null
    },

    // ./form.js inits the controls with options for layer and model
    initialize( options ) {
        Backform.Control.prototype.initialize.apply(this, arguments);
        this.layer = options.layer;
    },

    template: _.template([
        '<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
        '<div class="<%=Backform.controlsClassName%>">',
        '  <div class="geom-inline-editor" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> >',
        '    <%= value %>',
        '  </div>',
        '  <% if (helpMessage && helpMessage.length) { %>',
        '    <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
        '  <% } %>',
        '</div>'
    ].join('\n')),

    events: _.extend({}, Backform.Control.prototype.events, {
        'click': 'onClick',
        'dblclick': 'onClick',
    }),

    onClick(e){
        if ( e ) e.preventDefault();
        const { baseURL, suffix } = window.wpEditorL10n.tinymce;

        if ( this.editor ) return this;

        window.tinymce.EditorManager.overrideDefaults( {
            base_url: baseURL,
            suffix,
        } );

        if ( document.readyState === 'complete' ) {
            this.initEditor();
        } else {
            window.addEventListener( 'DOMContentLoaded', this.initEditor );
        }
    },

    setupToolbar(){
        if ( ! this.$toolbar ){
            // create toolbar element
            this.$toolbar = $('<div>', {
                id: 'geom-inline-editor-toolbar-' + this.cid,
                class: 'geom-inline-editor-toolbar freeform-toolbar',
                ['data-placeholder']: __( 'Classic' ),
            } );
            // append toolbar to container
            this.$el.closest('div[data-type="geom/map"]').find('.geom-toolbar').append(this.$toolbar);
            // animate toolbar
            let autoHeight = this.$toolbar.css('height', 'auto').height();
            this.$toolbar.height(0).animate({height: autoHeight}, 500, () => this.$toolbar.css('height', 'auto') );
            // toolbar events
            this.$toolbar.on('keydown', this.onToolbarKeyDown.bind(this) );
        }
    },

    getEditorElement(){
        return this.$el.find('.geom-inline-editor');
    },

    initEditor() {
        const { settings } = window.wpEditorL10n.tinymce;
        if ( this.editor ) return;
        // setup editor element
        this.getEditorElement().attr( 'id', 'editor-' + this.cid );
        // setup toolbar element
        this.setupToolbar();
        // initialize
        wp.oldEditor.initialize( 'editor-' + this.cid, {
            tinymce: {
            ...settings,
            inline: true,
            content_css: geomData.pluginDirUrl + '/css/geom_block_map_editor_tinymce_content.min.css',
            fixed_toolbar_container: '#geom-inline-editor-toolbar-' + this.cid,
            setup: this.onSetup.bind(this),
        },
        } );
    },

    onSetup( editor ) {
        const self = this;
        const content  = this.getEditorElement().html();
        this.editor = editor;

        editor.addButton( 'kitchensink', {
            tooltip: __( 'More' ),
            icon: 'dashicon dashicons-editor-kitchensink',
            onClick: function() {
                const button = this;
                const active = ! button.active();
                button.active( active );
                editor.dom.toggleClass( self.$toolbar, 'has-advanced-toolbar', active );
            },
        } );

        if ( content ) {
            editor.on( 'loadContent', () => editor.setContent( content ) );
        }

        editor.on( 'init', () => {
            // Create the toolbar by refocussing the editor.
            editor.getBody().blur();
            editor.focus();
        } );

        // // ??? well that doesn't work... will fix that in future
        // editor.on('keydown', ( event ) => {
        //  const { altKey } = event;
        //  // Prevent Mousetrap from kicking in: TinyMCE already uses its own 'alt+f10' shortcut to focus its toolbar.
        //  // if ( altKey && event.keyCode === F10 ) {
        //  if ( event.keyCode === F10 ) {
        //      event.stopPropagation();
        //  }
        // });

        editor.on( 'blur', (event) => {
            this.setModelVal(event);
            return false;
        } );

        editor.on('KeyUp Change Paste input touchend', ( event ) => {
            // close editor if esc pressed
            if ( event.keyCode === ESCAPE ) {
                this.close(event);
            }
        });

        editor.on('focusout', ( event ) => {
            if ( undefined !== $( event.explicitOriginalTarget ) ){

                if ( $( event.explicitOriginalTarget ).attr('id') ){
                    if ( $( event.explicitOriginalTarget ).attr('id').startsWith('mce') ){
                        return;
                    }
                }

                if ( event.explicitOriginalTarget.tagName === 'BUTTON' ){
                    this.setModelVal(event);
                    this.close(event);
                    $( event.explicitOriginalTarget ).trigger('click');
                    return;
                }
            }
            this.setModelVal(event);
            this.close(event);
        });
    },

    focus() {
        if ( this.editor ) {
            this.editor.focus();
        }
    },

    onToolbarKeyDown( event ) {
        // Prevent WritingFlow from kicking in and allow arrows navigation on the toolbar.
        event.stopPropagation();
        // Prevent Mousetrap from moving focus to the top toolbar when pressing 'alt+f10' on this block toolbar.
        event.nativeEvent.stopImmediatePropagation();
    },

    close(e){
        if ( e ) e.preventDefault();
        this.removeEditor();
    },

    setModelVal(e){
        if ( e ) e.preventDefault();
        const model = this.model;
        const val = this.editor.getContent();
        const oldVal = model.get( this.field.get( 'name' ) ) || model.get( this.field.get( 'name' ) ).rendered;
        const newVal = this.formatter.toRaw( val ) || this.formatter.toRaw( val ).rendered;
        if ( ! _.isUndefined( newVal ) ) this.model.set( 'content', newVal );
    },

    getValueFromDOM() {
        return this.formatter.toRaw( this.getEditorElement().html(), this.model );
    },

    removeEditor() {
        if ( this.editor ){
            window.addEventListener( 'DOMContentLoaded', this.initEditor );
            wp.oldEditor.remove( 'editor-' + this.cid );
            this.removeToolbar();
            delete this.editor;
            this.getEditorElement().attr( 'id', null);
        }
    },

    removeToolbar(){
        if ( this.$toolbar ){
            this.$toolbar.animate({height: 0}, 500, () => {
                this.$toolbar.remove();
                delete this.$toolbar;
            });
        }
    },

});

export default WysiwygControl;

New Plugin Version 0.0.7

… well the best way would be to build everything in React. But thats another story

Update 10 July 2018: Updated plugin links to version 0.0.6 (compatible with gb 3.2.0 and wp 4.9.7)
Update 12 July 2018: Updated plugin links to version 0.0.7 (compatible with gb 3.2.0 and wp 4.9.7)

Leave a Comment