Level of experience | ADVANCED |
Time estimate | 0:45 |
Atlassian application | JIRA 7.3+ |
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:
The core components of our macro are:
Those parts are described in Tutorial - Writing plugin for Rich Text Editor in JIRA.
Listing of InfoMacro.java
1 2package 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); } } }
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 p
or strong
tags.
Listing of info-macro.less
1 2info-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; } }
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 2AJS.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; }); }; });
Listing of ref-i18n.properties
1 2refplugin.toolbar.info=Info refplugin.macro.info.placeholder=Info...
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 2require([ "jira/editor/customizer" ], function ( Customizer ) { Customizer.customizeSettings(function (tinymceSettings, tinymce, SchemaBuilder) { SchemaBuilder.withCustomElement('info-macro', ['p', 'ul', 'ol']); }); });
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 schema
, valid_elements
, valid_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.
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.
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 2require([ "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>' })); } }); }; });
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.
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>
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>
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>
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" />
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>
Listing. A piece of atlassian-plugin.xml
1 2<resource type="i18n" name="i18n" location="ref-i18n"/>
Rate this page: