Tutorial - Customizing Rich Text Editor in JIRA

Level of experience ADVANCED
Time estimate 0:45
Atlassian application JIRA 7.3+

On this page:

Feature overview

If you completed Tutorial - Writing plugin for Rich Text Editor in JIRA, you will have learnt how to add new functionality to the Rich Text Editor. After reading this tutorial you will know how to add a macro which uses the new custom element. Let's assume we want to create a simple {info} macro. The end result:

Let's dive into the details.

Presented example has the following code structure:

Creating info macro

The core components of our macro are:

  • InfoMacro – renderer class,
  • soy templates - HTML and Wiki markup,
  • CSS – look & feel,
  • tests – to be sure everything is working as you expect,
  • i18n – copy, text which is visible to the user.

Those parts are described in Tutorial - Writing plugin for Rich Text Editor in JIRA.

Adding InfoMacro – renderer class

Listing of InfoMacro.java
package com.atlassian.jira.plugin.editor.ref;

import com.atlassian.jira.template.soy.SoyTemplateRendererProvider;
import com.atlassian.renderer.RenderContext;
import com.atlassian.renderer.v2.RenderMode;
import com.atlassian.renderer.v2.macro.BaseMacro;
import com.atlassian.renderer.v2.macro.MacroException;
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.google.common.collect.ImmutableMap;

import java.util.Map;


public class InfoMacro extends BaseMacro {

    private static final String FALLBACK_RENDER_OUTPUT = "{info}%s{info}";

    private final SoyTemplateRenderer soyTemplateRenderer;

    public InfoMacro(final SoyTemplateRendererProvider soyTemplateRendererProvider) {
        super();
        this.soyTemplateRenderer = soyTemplateRendererProvider.getRenderer();
    }

    @Override
    public boolean hasBody() {
        return true;
    }

    @Override
    public RenderMode getBodyRenderMode() {
        return RenderMode.allow(RenderMode.F_ALL);
    }

    @Override
    public String execute(Map<String, Object> parameters, String body, RenderContext renderContext) throws MacroException {

        ImmutableMap.Builder<String, Object> templateParams = ImmutableMap.builder();
        templateParams.put("content", body);

        try {
            return this.soyTemplateRenderer.render(
                    "com.atlassian.jira.plugins.jira-editor-ref-plugin:handler",
                    "RefPlugin.Macros.Info.html",
                    templateParams.build());
        } catch (SoyException e) {
            return String.format(FALLBACK_RENDER_OUTPUT, body);
        }
    }
}

Adding soy templates - HTML and Wiki markup

Listing of info-macro.soy
 
{namespace RefPlugin.Macros.Info}
/**
 * @param content
 */
{template .html}
    <info-macro>{$content|noAutoescape}</info-macro>
{/template}

/**
 * @param content
 */
{template .wiki}
    {lb}info{rb}{$content}{lb}info{rb}
{/template}

/**
 * @param innerMarkup
 */
{template .convert}
    {lb}info{rb}{$innerMarkup}{lb}info{rb}
{/template}

Note that inside the html template noAutoescape modifier is used because we want to facilitate putting true HTML there, for example or strong tags.

Adding CSS – look & feel

Listing of info-macro.less
info-macro {
  @text-padding: 18px;
  @icon-indent: -(@text-padding - 3px);

  margin: @text-padding/4 0;
  padding: 10px 4px 4px 4px;
  display: block;
  background: #3572b0;

  > * {
    padding: 0 @text-padding @text-padding/4 @text-padding !important;
    background: #fff !important;
    margin: 0 !important;
  }

  > *:first-child:before {
    content: "\2139";
    font-size: 22px;
    text-indent: 0;
    margin-left: @icon-indent;
    color: inherit;
    font-weight: 400;
    -webkit-font-smoothing: antialiased;
    font-style: normal;
    speak: none;
  }
}

Adding tests

This section contains only the example of a few simple unit tests.

For more details, please refer to the Running and testing section of Tutorial - Writing plugin for Rich Text Editor in JIRA.

Listing of info-macro-init.js
AJS.test.require(['com.atlassian.jira.plugins.jira-editor-ref-plugin:handler'], function () {
    var htmlConverter = require('jira/editor/converter');

    module('InfoMacro handler');

    test('Should convert HTML to wiki markup for info macro properly', function () {
        assertConversion('<info-macro><p>WIP</p></info-macro>', '{info}WIP{info}');
        assertConversion('<info-macro><p>WIP<br>new line</p></info-macro>', '{info}WIP\nnew line{info}');
        assertConversion('<info-macro><p>one two</p><ul><li>a</li><li>b</li></ul></info-macro>', '{info}one two\n * a\n * b{info}');
    });

    var assertConversion = function (html, markup, testName) {
        htmlConverter.convert(html).then(function (result) {
            equal(result, markup, testName);
        }).fail(function (e) {
            throw e;
        });
    };
});

Adding i18n – copy

Listing of ref-i18n.properties
refplugin.toolbar.info=Info
refplugin.macro.info.placeholder=Info...

Allowing custom element to be used inside editor

We're using new custom element info-macro and TinyMCE doesn't know anything about this tag, thus we need to tell TinyMCE how to support the tag.

We need a JS file for that, which will handle the initialisation of info macro. Let's call it info-macro-init.js.

Listing of info-macro-init.js
require([
    "jira/editor/customizer"
], function (
    Customizer
) {
    Customizer.customizeSettings(function (tinymceSettings, tinymce, SchemaBuilder) {
        SchemaBuilder.withCustomElement('info-macro', ['p', 'ul', 'ol']);
    });
}); 
  What's going on here?
  • We are calling require to be able to use Customizer since it allows us to add
  • Given callback function (tinymceSettings, tinymce, SchemaBuilder) { ... } will be called before TinyMCE editor instance is initialised.
    There are three parameters we can use:
    • tinymceSettings object which is used for initialising TinyMCE editor instance: tinymce.init(tinymceSettings);
      (for more details take a look at the TinyMCE documentation

    • tinymce TinyMCE main object which we can use for example to add new TinyMCE plugin
      (some examples are provided in this part of TinyMCE documentation)
    • SchemaBuilder Rich Text Editor controls TinyMCE schema-related settings such as schemavalid_elementsvalid_children or custom_elements because only the subset of HTML is supported by Wiki Markup format which is used as a storage format in JIRA.

  • SchemaBuilder is used to add info-macro custom element along with the allowed children: p, ul, ol.

tinymceSettings allows to modify all TinyMCE settings but schema, valid_elements, extended_valid_elements, valid_children and custom_elements. You can use SchemaBuilder to alter those properties. Currently, only withCustomElement method is exposed which allows you to add custom element along with allowed children and attributes.

/**
 * Registers a custom element along with allowed children.
 * The text node is added by default.
 *
 * @example
 * withCustomElement('x-task', ['p']);
 * withCustomElement('x-task', false);
 * withCustomElement('x-task');
 * withCustomElement('x-task', ['p'], ['content', 'done']);
 *
 * @param {string} name custom element name
 * @param {array.string|false=} children
 * when array provided: tag names of allowed children;
 * when false: no children allowed;
 * when undefined / unspecified: allow p and #comment nodes
 * @param {array.string=} attributes list of allowed attributes
 * @returns {SchemaBuilder}
 */
SchemaBuilder.prototype.withCustomElement = function (name, children, attributes);

Tip

There are some problems when dealing with text nodes directly under the custom element. For better user experience, the content of your macro should be wrapped into p tag.

Attaching info macro to the toolbar

We need to add a button to the toolbar and also implement proper action when user clicks it. This part is covered in Tutorial - Writing plugin for Rich Text Editor in JIRA.

Below you can find the extended info-macro-init.js file.

Listing of info-macro-init.js
require([
    "jquery",
    "jira/util/formatter",
    "jira/editor/registry",
    "jira/editor/customizer"
], function (
    $,
    formatter,
    editorRegistry,
    Customizer
) {
    var RefPlugin = window.RefPlugin;

    Customizer.customizeSettings(function (tinymceSettings, tinymce, SchemaBuilder) {
        SchemaBuilder.withCustomElement('info-macro', ['p', 'ul', 'ol']);
    });

    var INFO = formatter.I18n.getText('refplugin.toolbar.info');
    var INFO_PLACEHOLDER = formatter.I18n.getText('refplugin.macro.info.placeholder');
    var DROPDOWN_ITEM_HTML = '<li><a href="#" data-operation="info">' + INFO + '</a></li>';

    editorRegistry.on('register', function (entry) {
        var $otherDropdown = $(entry.toolbar).find('.wiki-edit-other-picker-trigger');

        $otherDropdown.one('click', function (dropdownClickEvent) {
            var dropdownContentId = dropdownClickEvent.currentTarget.getAttribute('aria-owns');
            var dropdownContent = document.getElementById(dropdownContentId);
            var speechItem = dropdownContent.querySelector('.wiki-edit-speech-item');

            var infoItem = $(DROPDOWN_ITEM_HTML).insertAfter(speechItem).on('click', function () {
                entry.applyIfTextMode(addWikiMarkup).applyIfVisualMode(addRenderedContent);
            });

            entry.onUnregister(function cleanup() {
                infoItem.remove();
            });
        });
    });

    function addWikiMarkup(entry) {
        var wikiEditor = $(entry.textArea).data('wikiEditor');
        var content = wikiEditor.manipulationEngine.getSelection().text || INFO_PLACEHOLDER;
        wikiEditor.manipulationEngine.replaceSelectionWith(RefPlugin.Macros.Info.wiki({content: content}));
    }

    function addRenderedContent(entry) {
        entry.rteInstance.then(function (rteInstance) {
            var tinyMCE = rteInstance.editor;
            if (tinyMCE && !tinyMCE.isHidden()) {
                var content = tinyMCE.selection.getContent() || INFO_PLACEHOLDER;
                tinyMCE.selection.setContent(RefPlugin.Macros.Info.html({ content: '<p>' + content + '</p>' }));
            }
        });
    };
});

Loading resources into editor context

All resources have to be reflected in atlassian-plugin.xml file. In this section, we will see step-by-step how to load info macro resources properly. The order of xml tags is arbitrary.

You can find the explanation of the particular xml tags used in this section in Tutorial - Writing plugin for Rich Text Editor in JIRA.

Putting info macro in place 

Listing. A piece of atlassian-plugin.xml
<macro key='info' name='{info} formatting macro'
       class='com.atlassian.jira.plugin.editor.ref.InfoMacro'>
    <description>Allows you to insert a information banner.</description>
    <param name="convert-selector">info-macro</param>
    <param name="convert-function">RefPlugin.Macros.Info.convert</param>
</macro>

Adding info-macro-init.js

Listing. A piece of atlassian-plugin.xml
<web-resource key="info-init" name="JIRA Editor Reference Plugin Info Macro Init">
    <context>jira.rich.editor</context>

    <context>jira.create.issue</context>
    <context>jira.view.issue</context>
    <context>jira.edit.issue</context>
    <context>gh-rapid</context>

    <dependency>${ref.plugin.key}:handler</dependency>

    <resource type="download" name="js/info-macro-init.js" location="js/info-macro-init.js"/>

    <transformation extension="js">
        <transformer key="jsI18n"/>
    </transformation>
</web-resource>

Adding HTML and Wiki markup info-macro.soy.js

Listing. A piece of atlassian-plugin.xml
<web-resource key="handler" name="JIRA Editor Reference Plugin Context Init">
    <context>jira.rich.editor</context>

    <dependency>com.atlassian.jira.plugins.jira-editor-plugin:converter</dependency>

    <resource name="soy/info-macro.soy.js" type="download" location="soy/info-macro.soy" />

    <transformation extension="soy">
        <transformer key="soyTransformer"/>
    </transformation>
</web-resource>

Adding unit tests info-macro-tests.js

Listing. A piece of atlassian-plugin.xml
<resource type="qunit" name="js/info-macro-tests.js" location="/js/info-macro-tests.js" />

Adding CSS info-macro.less

Listing. A piece of atlassian-plugin.xml
<web-resource key="css" name="JIRA Editor Reference Plugin CSS Resources">
    <context>jira.view.issue</context>
    <context>gh-rapid</context>

    <context>jira.rich.editor.content</context>

    <transformation extension="less">
        <transformer key="lessTransformer"/>
    </transformation>

    <resource type="download" name="less/info-macro.css" location="less/info-macro.less"/>
</web-resource>

Adding i18n

Listing. A piece of atlassian-plugin.xml
<resource type="i18n" name="i18n" location="ref-i18n"/>

Was this page helpful?

Have a question about this article?

See questions about this article

Powered by Confluence and Scroll Viewport