Last updated Nov 22, 2024

Customizing Rich Text Editor in JIRA

Level of experienceADVANCED
Time estimate0:45
Atlassian applicationJIRA 7.3+

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

1
2
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

1
2
 
{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

1
2
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

1
2
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

1
2
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

1
2
require([
    "jira/editor/customizer"
], function (
    Customizer
) {
    Customizer.customizeSettings(function (tinymceSettings, tinymce, SchemaBuilder) {
        SchemaBuilder.withCustomElement('info-macro', ['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.

1
2
/**
 * 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

1
2
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

1
2
<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

1
2
<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

1
2
<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

1
2
<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

1
2
<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

1
2
<resource type="i18n" name="i18n" location="ref-i18n"/>

Rate this page: