EPiServer TinyMCE 3 Wrapping selection in unsupported aside tag

For my current client, we had a need to allow EPiServer editors to wrap XHtmlSting field content in aside tags in a user friendly manner. The first most obvious thought went to the styles drop down list and EPiServer’s editor.css file, however after a few tries and a bit of Ilspy‘ing I realized that the aside tag element is not supported by the current implementation. If you can settle for a common p tag or perhaps a div for wrapping, everything is fine though.

So just adding a simple rule to the editor.css file would not do.

editor.css

aside.summary {
  EditMenuName: Summary;
  background: rgba(240, 30, 0, 0.05);
}

aside.facts {
  EditMenuName: Facts;
  background: rgba(87, 178, 217, 0.2);
}

Adding a custom TinyMCE button to EPiServer’s XHtml field to wrap in aside tags

So a TinyMCE button plugin would have to be the way to go. It is quite straight forward. First, create a structure in your web project mirroring EPiServer’s virtual path: {wwwroot}\Util\Editor\tinymce\plugins\{your_plugin_name}\

EPiServer's virtual path as a structure in your web project starting from the wwwroot.

Add the button icon images as in the image above (or place them in the recommended prettier structure); mine are facts.gif and summary.gif. Then add the two JavaScript files editor_plugin.js and editor_plugin_src.js; the former is just a minified version of the latter and is the one being used by TinyMCE.

Here is the unminified version of the JavaScript code. Basically, all it does is checking whether or not it should add or remove the aside element, and how much should be included in the wrapping.

editor_plugin_src.js

(function (tinymce, $) {
tinymce.create('tinymce.plugins.colorpicker', {
  init: function (ed, url) {

    function isConsideredEmpty(node) {
      return (node.innerHTML === "&nbsp;" || node.innerHTML === "" || node.innerHTML === " " || node.innerHTML === "<br>");
    }

    function toggleWrapping(ed, cssClass) {
      var selected = ed.selection.getNode();
      var content = ed.selection.getContent();
      var aside = ed.dom.getParent(selected, "aside");

      if (aside === null || typeof aside === "undefined") {
        // Should wrap selected content in aside-tag.
        var isSelectedSingleTag = !selected.innerHTML.startsWith("<");
        var isPartiallySelectedTag = content.length < selected.innerHTML.length;
        if (isSelectedSingleTag && isPartiallySelectedTag) {
          tinymce.activeEditor.selection.select(selected);
        }

        if (isSelectedSingleTag) {
          content = selected.outerHTML;
        }

        ed.selection.setContent("<aside id='d73Jb2bb' class='" + cssClass + "'>" + content + "</aside>");
        aside = ed.dom.get("d73Jb2bb");
        ed.dom.setAttrib(aside, "id", "");

        var previous = aside.previousSibling;
        var next = aside.nextSibling;
        if (isConsideredEmpty(previous)) {
          ed.dom.remove(previous);
        }
        if (isConsideredEmpty(next)) {
          ed.dom.remove(next);
        }

        ed.undoManager.add();
        return;
      }

      // Should remove aside-tag wrapping.
      for (var i = aside.childNodes.length - 1; i >= 0; i--) {
        var node = aside.childNodes[i];
        ed.dom.insertAfter(node, aside);
      }
      ed.dom.remove(aside);
      ed.undoManager.add();
    }

The next part of this script is the registration of the buttons. Below we register two, one for the summary background color, and one for the facts one.

    // Register facts button
    ed.addButton("factsbutton", {
      title: "Facts",
      image: url + "/facts.gif",
      onclick: function () {
        ed.focus();
        toggleWrapping(ed, "facts");
      }
    });

    // Register summary button
    ed.addButton("summarybutton", {
      title: "Summary",
      image: url + "/summary.gif",
      onclick: function () {
        ed.focus();
        toggleWrapping(ed, "summary");
      }
    });
    },

    getInfo: function () {
      return {
        longname: "Color picker",
        author: "Mathias",
        authorurl: "https://blog.mathiaskunto.se",
        infourl: "https://blog.mathiaskunto.se",
        version: tinymce.majorVersion + "." + tinymce.minorVersion
      };
    }
  });

  // Register plugin
  tinymce.PluginManager.add("colorpicker", tinymce.plugins.colorpicker);
})(tinymce, epiJQuery);

Add a little meta information as above, and register the plugin with the TinyMCE plugin manager.

For EPiServer to set things up, we also need to create a class decorated with the TinyMCEPluginButtonAttribute specifying where it can expect to find resources.

[TinyMCEPluginButton(
  PlugInName = "colorpicker",
  ButtonName = "factsbutton",
  GroupName = "misc",
  LanguagePath = "/tinymce/colorpicker/factsbutton",
  IconUrl = "Editor/tinymce/plugins/colorpicker/facts.gif")]
  public class ColorPickerPluginFactsButton
  {
  }

[TinyMCEPluginButton(
  PlugInName = "colorpicker",
  ButtonName = "summarybutton",
  GroupName = "misc",
  LanguagePath = "/tinymce/colorpicker/summarybutton",
  IconUrl = "Editor/tinymce/plugins/colorpicker/summary.gif")]
  public class ColorPickerPluginSummaryButton
  {
  }

Be sure to set up translations for the button descriptions, in whatever manner you’re doing it.

<?xml version="1.0" encoding="utf-8" ?>
<languages xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <language>
    <tinymce>
      <colorpicker>
        <factsbutton_desc>Create facts area with selected text.</factsbutton_desc>
        <summarybutton_desc>Create summary area with selected text.</summarybutton_desc>
      </colorpicker>
    </tinymce>

The result of this will be the two toggle buttons on the upper right in the image below. They will wrap the selected markup in an aside element, or remove the tag where the cursor is.

EPiServer Xhtml editor with two new buttons.