Custom buttons on tinymce_4_0_15

Feb 11, 2014 at 4:19 PM
Hi All!

I am using tinymce_4_0_15 on Blogengine 2.8.

I am able to show my custom button on the toolbar. When the user clicks on the drop-down of my custom button, I'm able to show the color pallette. When the user clicks on one of the colors, I want to set the backgroundcolor of the post to the chosen color.

I know the color selection is working because the chosen color shows on the button. Just don't know which command to call to set the backgroundcolor. I'm thinking I need to set the style of the Post but just can't see any example out there.

Any CSS expert out there?

Thanks!
Feb 12, 2014 at 1:17 PM
I was curious, so gave it a try.

Editor, Post

You do need to set the style of the post, but you can't do that directly from the editor(as far as I can see).
Here's what worked for me.

• Modify the tinymce "textcolor" plugin adding another colour button that sets editor background colour and saves details of that change to a hidden content element.
• To persist the changes to the editor background colour for each post, modify init() in Admin/tinyMCE.ascx to extract colour info from hidden element and set editor background colour.
• When the post is run, grab color info from hidden element and replace that element with content wrapping div styled using hidden element's color. I tested this with an extension, but parse/replace at runtime anyway that suits.

Code changes are commented

Opened textcolor plugin.min and replaced code with following - uncompressed(changes at end).
/**
 * plugin.js
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/*global tinymce:true */

tinymce.PluginManager.add('textcolor', function (editor) {
    function mapColors() {
        var i, colors = [], colorMap;

        colorMap = editor.settings.textcolor_map || [
            "000000", "Black",
            "993300", "Burnt orange",
            "333300", "Dark olive",
            "003300", "Dark green",
            "003366", "Dark azure",
            "000080", "Navy Blue",
            "333399", "Indigo",
            "333333", "Very dark gray",
            "800000", "Maroon",
            "FF6600", "Orange",
            "808000", "Olive",
            "008000", "Green",
            "008080", "Teal",
            "0000FF", "Blue",
            "666699", "Grayish blue",
            "808080", "Gray",
            "FF0000", "Red",
            "FF9900", "Amber",
            "99CC00", "Yellow green",
            "339966", "Sea green",
            "33CCCC", "Turquoise",
            "3366FF", "Royal blue",
            "800080", "Purple",
            "999999", "Medium gray",
            "FF00FF", "Magenta",
            "FFCC00", "Gold",
            "FFFF00", "Yellow",
            "00FF00", "Lime",
            "00FFFF", "Aqua",
            "00CCFF", "Sky blue",
            "993366", "Brown",
            "C0C0C0", "Silver",
            "FF99CC", "Pink",
            "FFCC99", "Peach",
            "FFFF99", "Light yellow",
            "CCFFCC", "Pale green",
            "CCFFFF", "Pale cyan",
            "99CCFF", "Light sky blue",
            "CC99FF", "Plum",
            "FFFFFF", "White"
        ];

        for (i = 0; i < colorMap.length; i += 2) {
            colors.push({
                text: colorMap[i + 1],
                color: colorMap[i]
            });
        }

        return colors;
    }

    function renderColorPicker() {
        var ctrl = this, colors, color, html, last, rows, cols, x, y, i;

        colors = mapColors();

        html = '<table class="mce-grid mce-grid-border mce-colorbutton-grid" role="presentation" cellspacing="0"><tbody>';
        last = colors.length - 1;
        rows = editor.settings.textcolor_rows || 5;
        cols = editor.settings.textcolor_cols || 8;

        for (y = 0; y < rows; y++) {
            html += '<tr>';

            for (x = 0; x < cols; x++) {
                i = y * cols + x;

                if (i > last) {
                    html += '<td></td>';
                } else {
                    color = colors[i];
                    html += (
                        '<td>' +
                            '<div id="' + ctrl._id + '-' + i + '"' +
                                ' data-mce-color="' + color.color + '"' +
                                ' role="option"' +
                                ' tabIndex="-1"' +
                                ' style="' + (color ? 'background-color: #' + color.color : '') + '"' +
                                ' title="' + color.text + '">' +
                            '</div>' +
                        '</td>'
                    );
                }
            }

            html += '</tr>';
        }

        html += '</tbody></table>';

        return html;
    }

    function onPanelClick(e) {
        var buttonCtrl = this.parent(), value;

        if ((value = e.target.getAttribute('data-mce-color'))) {
            buttonCtrl.hidePanel();
            value = '#' + value;
            buttonCtrl.color(value);
            editor.execCommand(buttonCtrl.settings.selectcmd, false, value);
        }
    }

    function onButtonClick() {
        var self = this;
        if (self._color) {
            editor.execCommand(self.settings.selectcmd, false, self._color);
        }
    }   

    editor.addButton('forecolor', {
        type: 'colorbutton',
        tooltip: 'Text color',
        selectcmd: 'ForeColor',
        panel: {
            html: renderColorPicker,
            onclick: onPanelClick
        },
        onclick: onButtonClick
    });

    editor.addButton('backcolor', {
        type: 'colorbutton',        
        tooltip: 'Background color', 
        selectcmd: 'HiliteColor',
        panel: {
            html: renderColorPicker,
            onclick: onPanelClick
        },
        onclick: onButtonClick
    });

    /********** Added ***************/

    function onBGPanelClick(e) {
        var buttonCtrl = this.parent(),
            value;

        if ((value = e.target.getAttribute('data-mce-color'))) {
            buttonCtrl.hidePanel();
            value = '#' + value;
            buttonCtrl.color(value);
            insertMarker(value);
        }
    }

    function onBGButtonClick() {

        var self = this;

        if (self._color) {
            insertMarker(self._color);
        }
    }

    function insertMarker(c) {

        var markerElement = editor.dom.get("pgBG"),
            el;

        editor.dom.setStyle(editor.dom.getRoot(), 'background-color', c);

        // Add marker or set color - ensure element gets added only once.
        if (markerElement) {
            markerElement.className = c;
        } else {
            el = editor.dom.create('p', { id: 'pgBG', 'class': c, style: 'display:none;' }, '');
            editor.getBody().insertBefore(el, editor.getBody().firstChild);
        }
    }

    editor.addButton('pagecolor', {
        type: 'colorbutton',
        tooltip: 'Page color',
        panel: {
            html: renderColorPicker,
            onclick: onBGPanelClick
        },
        onclick: onBGButtonClick
    });


    /******** End Added *************/

});
Typical tinyMCE.ascx, commented changes at end.
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="tinyMCE.ascx.cs" Inherits="Admin.TinyMce" %>
<%@ Import Namespace="BlogEngine.Core" %>

<script type="text/javascript" src="<%=Utils.RelativeWebRoot %>editors/tiny_mce_3_3_9_2/tinymce.min.js"></script>
<script type="text/javascript">

    tinyMCE.init({
        ...       

        /******** pagecolor button added to toolbar ********/
        toolbar: "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link | print preview fullpage | forecolor backcolor pagecolor",

        style_formats: [
             { title: 'Bold text', inline: 'b' },
             { title: 'Red text', inline: 'span', styles: { color: '#ff0000' } },
             { title: 'Red header', block: 'h1', styles: { color: '#ff0000' } },
             { title: 'Example 1', inline: 'span', classes: 'example1' },
             { title: 'Example 2', inline: 'span', classes: 'example2' },
             { title: 'Table styles' },
             { title: 'Table row 1', selector: 'tr', classes: 'tablerow1' }
        ],

        /******** Persist any previously selected bg color ********/
        // Don't forget the preceding comma above
        setup: function (editor) {
            editor.on('init', function (e) {
                var markerElement = editor.dom.get("pgBG");

                if (markerElement && markerElement.className) {

                    editor.dom.setStyle(editor.dom.getRoot(), 'background-color', markerElement.className);
                }
            });
        }
        /********  End add setup ********/
    });
    
</script>

<asp:TextBox runat="Server" ID="txtContent" CssClass="post" Width="100%" Height="250px" TextMode="MultiLine" />
Test extension PageColor.cs won't fit here, see next.
Feb 12, 2014 at 1:18 PM
PageColor.cs
#region using

using System;
using System.Text.RegularExpressions;
using System.Web;

using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using BlogEngine.Core.Web.Extensions;

#endregion

/// <summary>
/// Replaces marker element with content wrapper div.
/// </summary>
[Extension(
    "Use marker element attribute to set colour on content wrapping div, div replaces marker",
    "1.0",
    "BlogEngine.NET",
    1010)]
public class PageColor
{
    #region Constructors and Destructors

    /// <summary>
    /// Initializes static members of the <see cref="BreakPost"/> class. 
    ///     Hooks up an event handler to the Post.Serving event.
    /// </summary>
    static PageColor()
    {
        Post.Serving += PostServing;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Replaces the marker with a content wrapping div.
    /// </summary>    
    /// <param name="e">
    /// The event arguments.
    /// </param>
    private static void ReplaceMarker(ServingEventArgs e)
    {
        Match regMarker = Regex.Match(e.Body, @"<p id=""pgBG"".*>(.+?)</p>");
        if (regMarker.Success)
        {
            if (e.Location == ServingLocation.Feed)
            {
                e.Body.Replace(regMarker.Value, string.Empty);                
            }
            else
            {
                Match regClass = Regex.Match(regMarker.Value, @"class=""(.*?)""");
                if (regClass.Success)
                {
                    e.Body = e.Body.Replace(regMarker.Value, string.Format("<div style='background-color:{0};'>", regClass.Groups[1]));
                    e.Body = e.Body + "</div>";
                }
                else
                {
                    e.Body.Replace(regMarker.Value, string.Empty);
                }
            }
        }        
    }

    /// <summary>
    /// Handles the Serving event of the Post control.   
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="BlogEngine.Core.ServingEventArgs"/> instance containing the event data.</param>
    private static void PostServing(object sender, ServingEventArgs e)
    {
        if (ExtensionManager.ExtensionEnabled("PageColor"))
        {
            ReplaceMarker(e);
        }
    }

    

    #endregion
}
Feb 12, 2014 at 3:15 PM
Hi Andy!

Thanks for the code. It works great.

Just so I understand this, this doesn't work with tinymce's Preview, correct? Unless I did something wrong.
Feb 12, 2014 at 7:07 PM
Looks like you need to tell preview plugin to include the style, following seems to work.

Pasted this over preview/plugin.min to test.
/**
 * plugin.js
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/*global tinymce:true */

tinymce.PluginManager.add('preview', function (editor) {
    var settings = editor.settings;

    editor.addCommand('mcePreview', function () {
        editor.windowManager.open({
            title: 'Preview',
            width: parseInt(editor.getParam("plugin_preview_width", "650"), 10),
            height: parseInt(editor.getParam("plugin_preview_height", "500"), 10),
            html: '<iframe src="javascript:\'\'" frameborder="0"></iframe>',
            buttons: {
                text: 'Close',
                onclick: function () {
                    this.parent().parent().close();
                }
            },
            onPostRender: function () {
                var doc = this.getEl('body').firstChild.contentWindow.document, previewHtml, headHtml = '';

                tinymce.each(editor.contentCSS, function (url) {
                    headHtml += '<link type="text/css" rel="stylesheet" href="' + editor.documentBaseURI.toAbsolute(url) + '">';
                });

                var bodyId = settings.body_id || 'tinymce';
                if (bodyId.indexOf('=') != -1) {
                    bodyId = editor.getParam('body_id', '', 'hash');
                    bodyId = bodyId[editor.id] || bodyId;
                }

                var bodyClass = settings.body_class || '';
                if (bodyClass.indexOf('=') != -1) {
                    bodyClass = editor.getParam('body_class', '', 'hash');
                    bodyClass = bodyClass[editor.id] || '';
                }

                /********** Added ***************/
                var bodyStyle = '';
                var el = editor.dom.get("pgBG");
                if (el && el.className) {
                    bodyStyle = "style='background-color:" + el.className + ";'";
                }
                /******** End Added *************/

                /******** Body tag following altered to include bodyStyle  *************/
                previewHtml = (
                    '<!DOCTYPE html>' +
                    '<html>' +
                    '<head>' +
                        headHtml +
                    '</head>' +
                    '<body id="' + bodyId + '" class="mce-content-body ' + bodyClass + '"' + bodyStyle + '>' +
                        editor.getContent() + 
                    '</body>' +
                    '</html>'
                );
                /******** End altered *************/

                doc.open();
                doc.write(previewHtml);
                doc.close();
            }
        });
    });

    editor.addButton('preview', {
        title: 'Preview',
        cmd: 'mcePreview'
    });

    editor.addMenuItem('preview', {
        text: 'Preview',
        cmd: 'mcePreview',
        context: 'view'
    });
});
The other code is really just a first crack at getting things working, might need refining.
Undo for the background button needs looking at, came across repaint issues, so left that out.
Admittedly, not terribly familiar with tinymce but background button logic as a custom command(addCommand) might be the way to go.

If I get to that before you do, I'll post it.
Feb 12, 2014 at 8:11 PM
Hi Andy!

That works perfect for the preview. I wouldn't worry about the undo so much since setting the background color of the Post is the first thing the editor does.

Thanks!
Feb 14, 2014 at 4:11 PM
Hi Andy!

The PostBackgroundColor could be very useful if you use the right color. In your example, a better color would have been a light shade of blue to match the man's blue shirt in the image of the Post.

I hope you pushed your code to production with all the effort you put into it. I think you'll find it useful in certain scenarios.

Thanks again.
Feb 15, 2014 at 1:19 PM
Edited Feb 16, 2014 at 3:13 PM
Agreed, best that can be said for that shade of yellow is that it's bold.
Still using the older tinymce 3.x, got a couple of plugins I use that are incompatible with the new version. Intend spending some time getting more familiar with the workings of the new editor when I get round to reworking the plugins and makes sense to keep background button.

That being the case, I added a toggle background to the color button. Clicking underscore button toggles color off/start color on. Meaning you can play around with backgrounds via palette but have option to remove or revert to the original color.

This replaces textcolor plugin added code.
/********** Added ***************/
    function checkStartColor(markerElement) {

        // Simulate staic var and init once only.
        if (typeof this.startColor === 'undefined') {
            this.startColor = markerElement && markerElement.className ? markerElement.className : '';
        }
        return this.startColor;
    }

    function insertMarker(color) {
        var el = editor.dom.create('p', {id: 'pgBG', 'class': color, style: 'display:none;'}, '&nbsp;');
        editor.getBody().insertBefore(el, editor.getBody().firstChild);
    }

    function onBGPanelClick(e) {

        var markerElement = editor.dom.get('pgBG'),
            buttonCtrl = this.parent(),
            startColor = checkStartColor(markerElement),
            value, c;

        if ((value = e.target.getAttribute('data-mce-color'))) {
            buttonCtrl.hidePanel();
            c = '#' + value;
            buttonCtrl.color('#FFF');
            editor.dom.getRoot().style.backgroundColor = c;

            // Add marker or set color - ensure element gets added only once.
            if (markerElement) {
                markerElement.className = c;
            } else {
                insertMarker(c);
            }
            editor.undoManager.add();
        }
    }

    function onBGButtonClick() {

        var markerElement = editor.dom.get('pgBG'),
            startColor = checkStartColor(markerElement);

        if (markerElement) {
            editor.dom.remove(markerElement);
            editor.dom.getRoot().style.backgroundColor = 'transparent';
            if (startColor) {
                this.color(startColor);
            }
        } else if (startColor) {
            editor.dom.getRoot().style.backgroundColor = startColor;
            insertMarker(startColor);
            this.color('#FFF');
        }
        editor.undoManager.add();
    }

    editor.addButton('pagecolor', {
        type: 'colorbutton',
        tooltip: 'Page color',
        panel: {
            html: renderColorPicker,
            onclick: onBGPanelClick
        },
        onclick: onBGButtonClick
    });

    /******** End Added *************/
Edit: minor changes to above code, see 'Final tweak' below.
Feb 16, 2014 at 2:54 AM
Hi Andy!

OK, It's almost perfect. I just changed one line so that when you remove the background, the button's color would be white instead of light grey (which is one of of the background color selections).

From:

this.color('#808080');

To:

this.color('#FFFFFF');


Thanks a lot for all this work!
Feb 16, 2014 at 10:10 AM
Edited Feb 16, 2014 at 3:00 PM
Final tweak.

Was thinking that toggle button color should reflect what the bg color 'will be' when clicked(to be consistent with other buttons), that being either default (white unless overridden) or start color(from previously saved item). New code for that pasted over previous "Added" section above.

Undo/redo operates on snapshot basis and if bg color is changed just prior to one of these actions, marker is removed or added back but bg is not refreshed. Following change to tinyMCE.init() in .ascx file keeps keeps things visually in sync.
 /******** Persist any previously selected bg color and refresh bg on undo/redo ********/
 setup: function (editor) {

     editor.on('init', function (e) {
         var markerElement = editor.dom.get("pgBG");

         if (markerElement && markerElement.className) {
             editor.dom.getRoot().style.backgroundColor = markerElement.className;
         }
     });

     // Using transparent respects any custom default styling.
     editor.on('undo', function (e) {
         var markerElement = editor.dom.get("pgBG");
         editor.dom.getRoot().style.backgroundColor = markerElement && markerElement.className ? markerElement.className : 'transparent';
     });

     editor.on('redo', function (e) {
         var markerElement = editor.dom.get("pgBG");
         editor.dom.getRoot().style.backgroundColor = markerElement && markerElement.className ? markerElement.className : 'transparent';
     });
 }
 /********  End add setup ********/