Расширение текстового редактора rich text в JIRA

Уровень опыта

Продвинутый

Временная оценка

2 часа

Приложение Atlassian

JIRA 7.3+

                   

Обзор функций

В этом уроке вы хотели бы добавить некоторые новые функции в визуальный редактор в JIRA. Идея проста: есть макрос {footer}, который сможет отображать HTML-разметку с помощью пользовательских стилей CSS. Данная разметка:

РИСУНОК

Пользовательский рендеринг макросов

Во-первых, вам нужно добавить новый макро рендер (виализатор) в плагин:

  1. Определите новый элемент <macro> в atlassian-plugin.xml:

atlassian-plugin.xml


<macro key='footer' name='{footer} formatting macro'
       class='com.atlassian.jira.plugin.editor.ref.FooterMacro'>
    <description>Insert footer content with regards.</description>
    <param name="convert-selector">div.footer-macro:not(.header-macro)</param>
    <param name="convert-function">RefPlugin.Macros.Footer.convert</param>
</macro>

  1. Внедрение класса Java-среды рендеринга:

FooterMacro.java


package com.atlassian.jira.plugin.editor.ref;

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 java.util.Map;

public class FooterMacro extends BaseMacro {
    @Override
    public boolean hasBody() {
        return true;
    }

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

    @Override
    public String execute(Map parameters, String body, RenderContext renderContext) throws MacroException {
        return "
" + body + "
"; } }
,>
  1. Бонус: как мне выводить HTML-код для WYSIWYG? RenderContext поставляется с параметром: IssueRenderContext.WYSIWYG_PARAM, и его можно использовать для возврата другого результата в такой ситуации:

 FooterMacro.java


package com.atlassian.jira.plugin.editor.ref;

import com.atlassian.jira.issue.fields.renderer.IssueRenderContext;
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 java.util.Map;

public class FooterMacro extends BaseMacro {
    @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 {
        if (Boolean.TRUE.equals(renderContext.getParam(IssueRenderContext.WYSIWYG_PARAM))) {
            return "<div class=\"footer-macro editing\">" + body + "</div>";
        } else {
            return "<div class=\"footer-macro\">" + body + "</div>";
        }
    }
}

Преобразование HTML в разметку Wiki

Поскольку разметка Wiki является форматом хранения JIRA, нам нужно определить, как наша новая разметка HTML должна быть преобразована обратно в текст Wiki:

Добавьте новый <web-resource> в atlassian-plugin.xml:

Atlassian-plugin.xml


<web-resource key="handler" name="JIRA Editor Reference Plugin Context Init">
    <context>jira.rich.editor</context>

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

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

Предоставьте код конверсии, шаблон, который должен печатать соответствующую разметку wiki:

footer-macro.soy


 
{ namespace RefPlugin.Macros.Footer }

/**
 * @param node
 * @param innerMarkup
 */
{template .convert}
    {lb}footer{if $node.title}:title={$node.title}{/if}{rb}{$innerMarkup}{lb}footer{rb}
{/template}

В качестве альтернативы определите RefPlugin.Macros.Footer.convert как глобальную функцию со следующей сигнатурой:


function (params) {
    return '{footer'+(params.node.title ? ':title='+params.node.title:'')+'}'+params.innerMarkup+'{footer}';
}

Этот интерфейс подходит для более продвинутого использования.

Теперь, выполняя изменения в режиме Visual, ваш контент будет сохранен как действительный формат вики.

Загрузка веб-ресурсов в контекст редактора

Содержимое редактора загружается в <iframe>, который имеет базовые таблицы стилей JIRA. Мы также хотим предоставить наш набор CSS, чтобы убедиться, что наш новый виджет будет отображаться правильно.

  1. Добавьте новый <web-resource>, выделенный для ресурсов контента редактора:

Atlassian-plugin.xml


<web-resource key="content" name="JIRA Editor Reference Plugin Content Resources">
    <context>jira.view.issue</context>
    <context>jira.rich.editor.content</context>
    <context>gh-rapid</context>

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

    <resource type="download" name="less/footer-content.css" location="less/footer-content.less"/>
</web-resource>

  1. Создать файл таблицы стилей:

div.footer-macro {
  border-top: 1px #ccc solid;
  padding: 10px;
  background: #f0f0f0;
}

Как результат

РИСУНОК

Добавление новой кнопки в панель инструментов

Мы хотели бы добавить новую кнопку в раскрывающийся список:

РИСУНОК

Предостережение здесь заключается в том, что выпадающее меню перезагружается(рендеризируется), когда мы нажимаем его кнопку, поэтому нам нужно привязываться к событию выпадающего клика, чтобы добавить наш пользовательский пункт меню.

Мы могли бы использовать событие JIRA.Events.NEW_CONTENT_ADDED, но для настройки RTE удобнее использовать модуль jira / editor / registry.

toolbar-init.js


require([
    "jquery",
    "jira/util/formatter",
    "jira/editor/registry"
], function (
    $,
    formatter,
    editorRegistry
) {
    var FOOTER = formatter.I18n.getText('refplugin.toolbar.footer');
    var FOOTER_PLACEHOLDER = formatter.I18n.getText('refplugin.macro.footer.placeholder');
    var DROPDOWN_ITEM_HTML = '<li><a href="#" data-operation="footer">' + FOOTER + '</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 footerItem = $(DROPDOWN_ITEM_HTML).insertAfter(speechItem);
        });
    });
});

Этот код должен выполняться всякий раз, когда редактор может быть инициализирован:

atlassian-plugin.xml


<web-resource key="toolbar" name="JIRA Editor Reference Plugin Toolbar Init">
    <context>jira.rich.editor</context>

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

    <resource type="download" name="js/toolbar-init.js" location="js/toolbar-init.js"/>
</web-resource>

Реализация действия панели инструментов

Редактор Rich Text Editor в JIRA построен поверх библиотеки TinyMCE, и в этом примере мы используем API TinyMCE 4.

РИСУНОК

В обработчике событий клика нам нужно заменить текущий выбор на HTML-разметку макроса нижнего колонтитула. Если ничего не выбрано, введите текст заполнителя.

footer-init.js


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 footerItem = $(DROPDOWN_ITEM_HTML).insertAfter(speechItem).on('click', function () {
            entry.applyIfTextMode(addWikiMarkup).applyIfVisualMode(addRenderedContent);
        });

        entry.onUnregister(function cleanup() {
            footerItem.remove();
        });
    });
});
 
function addWikiMarkup(entry) {
    var wikiEditor = $(entry.textArea).data('wikiEditor');
    var content = wikiEditor.manipulationEngine.getSelection().text || FOOTER_PLACEHOLDER;
    wikiEditor.manipulationEngine.replaceSelectionWith(RefPlugin.Macros.Footer.wiki({content: content}));
}

function addRenderedContent(entry) {
    entry.rteInstance.then(function (rteInstance) {
        var tinyMCE = rteInstance.editor;
        if (tinyMCE && !tinyMCE.isHidden()) {
            var content = tinyMCE.selection.getContent() || FOOTER_PLACEHOLDER;
            tinyMCE.selection.setContent(RefPlugin.Macros.Footer.html({content: content}));
        }
    });
}

Здесь мы используем модуль jira / editor / registry для доступа к экземпляру редактора, здесь вы можете увидеть его «документацию». Сначала мы подписываемся на событие register, так что наш обратный вызов выполняется после того, как пользователь активирует редактор. Когда пользователь нажимает на наш элемент «Нижний колонтитул» (Footer), мы используем запись entry  для проверки текущего режима редактора и добавления соответствующей разметки.

addWikiMarkup добавляет разметку wiki для текстового режима, используя API текстовой области textarea, обернутой jira-wiki-editor-plugin.

addRenderedContent использует API TinyMCE 4 для вставки разметки HTML. rteInstance - это обещание, поскольку оно не инициализируется, пока мы не активируем визуальный режим.

вызов entry.onUnregister не нужен в этом примере, так как выпадающее меню перезагружено, это просто пример очистки материала после скрытия редактора (т. е. незарегистрированного из реестра).

Запуск и тестирование

Тесты веб-драйверов

JIRA поставляется с превосходной средой тестирования(фреймвоком) Selenium / WebDriver. Вы можете взаимодействовать с приложением через API объектов страниц, пожалуйста, прочитайте больше в руководстве по написанию интеграционных тестов с использованием PageObjects на Atlassian Developers.

Редактор Rich Text  для JIRA имеет собственные объекты страницы, вы можете добавить их в свой проект pom.xml:

pom.xml


<dependency>
    <groupId>com.atlassian.jira.plugins</groupId>
    <artifactId>jira-editor-pageobjects</artifactId>
    <version>1.2.7</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>com.atlassian.jira</groupId>
            <artifactId>atlassian-jira-pageobjects</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Начните с включения функции в тест setUp:

TestRefPlugin.java


@Before
public void setUp() throws Exception {
    backdoor.applicationProperties().setOption(APKeys.JIRA_OPTION_RTE_ENABLED, true);
}

В этом случае нам нужно создать новую задачу, использовать новую кнопку на панели инструментов, сохранить описание, а затем проверить с помощью бэкдора, каково фактическое состояние, хранящееся в базе данных:

TestRefPlugin.java


@Test
public void testToolbarCustomizationInVisualMode() {
    final String key = backdoor.issues().createIssue("HSP", "xxx").key;
    ExtendedViewIssuePage viewIssuePage = jira.goTo(ExtendedViewIssuePage.class, key);
    Poller.waitUntilTrue("Editable description should show when empty", viewIssuePage.hasEditableDescriptionTimed());

    DescriptionSectionRte descriptionSectionRte = viewIssuePage.editDescription(DescriptionSectionRte.class);
    final RichTextEditor richTextEditor = descriptionSectionRte.getRichTextEditor().switchMode(EditorMode.WYSIWYG);

    richTextEditor.clickToolbarButton(new ToolbarButton("Footer", "[data-operation='footer']"));

    Poller.waitUntilEquals("{footer}Footer...{footer}", richTextEditor.getTimedSource());
}

Проверка HTML → целостность разметки Wiki

Rich Text Editor для JIRA поставляется с мощным конвертером HTML → Wiki. Добавление новых NodeHandlers должно сопровождаться запуском набора тестов, который доступен по этому URL:

Рассмотрите возможность написания собственных тестов для нового макроса:

  1. Добавить ресурс qunit:

Atlassian-plugin.xml


<resource type="qunit" name="js/foooter-handler-tests.js" location="/js//foooter-handler-tests.js" />

  1. Внедрение простого тестового примера:

foooter-handler-tests.js


 AJS.test.require(['com.atlassian.jira.plugins.jira-editor-ref-plugin:handler'], function () {
    var htmlConverter = require('jira/editor/converter');
    var Strings = require('jira/editor/converter/util/strings');

    module('Footer macro Handler');
 
    test('Test footer macro', function () {
        assertConversion('<div class="footer-macro">footer</div>', '{footer}footer{footer}');
        assertConversion('<div class="footer-macro">footer with <b>Bold</b></div>', '{footer}footer with *Bold*{footer}');
    });

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

  1. Запуск тестов в браузере: foooter-handler-tests.js

Вопрос: Для настройки фреймвока и написания тестов требуется много времени, мне это нужно?

Ответ: Да! Представление регрессий для редактирования в JIRA может привести к отключению вашего плагина от ваших пользователей.

Завершение и последующие шаги

После прочтения этого урока вы сможете настроить панель инструментов, выполнить операции над редактором, добавить новый тип контента и проверить свои изменения.

Наш макрос {footer} может быть очарован новыми функциями, например идеями: конвертируйте {footer} для соответствующего HTML в визуальном режиме или с изменением цвета фона.

 

Ресурсы, которые могут вам помочь:

 

По материалам Atlassian JIRA  Server Developer Extending the rich text editor in JIRA