Skip to main content

Improving Confluence Data Center: Show Jira Issue Summary on Hover in Editor Mode

When editing pages with dozens of linked Jira issues in Confluence, Jira issue macro shows only key and no additional information of issue like summary, description. Figuring out what each issue actually refers to usually requires openning issue in new tab, this may be a little bit annoying

Two month ago my colleague asked me to fix this but at that time I considered that as minor fix. First of all, I tried to find suitable plugin for DC, but there is no any suitable.

As we know Confluence has application links that supports OAUTH, REST API within Atlassian products to handle this. The trick is to write some JS script, attach it to TinyMCE.

preview

Implementation
#

Go to Confluence Admin -> ScriptRunner-> Script Fragments.
Create a new Install Web Resource fragment.
Configure:Key: jira-macro-hover-summary
Contexts: editor
Resource: jira-editor-summary.js\

Save the following file in your ScriptRunner scripts folder (web-resources/com.onresolve.confluence.groovy.groovyrunner/jira-editor-summary.js).

(function ($) {
    'use strict';

    function findConfluenceEditor() {
        if (window.tinymce) {
            if (tinymce.activeEditor && tinymce.activeEditor.getDoc()) {
                return tinymce.activeEditor;
            }
            if (tinymce.editors && tinymce.editors.length > 0) {
                for (var i = 0; i < tinymce.editors.length; i++) {
                    if (tinymce.editors[i].getDoc()) {
                        return tinymce.editors[i];
                    }
                }
            }
        }
        if (window.AJS && AJS.RTE && typeof AJS.RTE.getEditor === 'function') {
            try {
                var ed = AJS.RTE.getEditor();
                if (ed && ed.getDoc()) return ed;
            } catch (e) {}
        }
        return null;
    }

    function injectOuterStyles() {
        var $topHead =$('head');
        if ($topHead.find('#lic-hover-outer-styles').length) return;

        var css = `
            #lic-hover-outer-dialog {
                display: none;
                position: absolute !important;
                z-index: 2147483647 !important;
                background-color: #ffffff !important;
                opacity: 1 !important;
                border: 1px solid #c1c7d0 !important;
                border-radius: 4px !important;
                box-shadow: 0 4px 16px rgba(9, 30, 66, 0.25), 0 0 1px rgba(9, 30, 66, 0.31) !important;
                padding: 10px 14px !important;
                width: 320px !important;
                max-width: 380px !important;
                pointer-events: none !important;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
                box-sizing: border-box !important;
            }
            #lic-hover-outer-dialog * {
                box-sizing: border-box !important;
            }
            .lic-dialog-header {
                display: flex !important;
                align-items: center !important;
                justify-content: space-between !important;
                margin-bottom: 6px !important;
                padding-bottom: 6px !important;
                border-bottom: 1px solid #ebecf0 !important;
            }
            .lic-dialog-key {
                font-weight: 700 !important;
                color: #0052cc !important;
                font-size: 12px !important;
            }
            .lic-dialog-status {
                background-color: #deebff !important;
                color: #0747a6 !important;
                padding: 2px 6px !important;
                border-radius: 3px !important;
                font-size: 10px !important;
                font-weight: 700 !important;
                text-transform: uppercase !important;
            }
            .lic-dialog-body {
                font-weight: 500 !important;
                color: #172b4d !important;
                font-size: 13px !important;
                line-height: 1.4 !important;
                word-wrap: break-word !important;
            }
        `;
        $topHead.append('<style id="lic-hover-outer-styles">' + css + '</style>');
    }

    function getMacroGlobalRect(editor, $macro) {
        var iframeEl = editor.iframeElement || editor.getWin().frameElement;
        var iframeRect = iframeEl.getBoundingClientRect();
        var macroRect = $macro[0].getBoundingClientRect();

        return {
            top: iframeRect.top + macroRect.top + $(window).scrollTop(),
            left: iframeRect.left + macroRect.left + $(window).scrollLeft(),
            height: macroRect.height
        };
    }

    function initJiraMacroHover(editor) {
        if (!editor || editor.__licHoverInitialized) return;
        editor.__licHoverInitialized = true;

        injectOuterStyles();

        var $topBody =$('body');
        var $dialog =$topBody.find('#lic-hover-outer-dialog');
        if (!$dialog.length) {$dialog = $('<div id="lic-hover-outer-dialog"></div>').appendTo($topBody);
        }

        var $iframeDoc = $(editor.getDoc());$iframeDoc.off('.licHover');

        $iframeDoc.on('mouseover.licHover', '*[data-macro-name="jira"]', function () {
            var $macro =$(this);

            if ($macro.data('lic-summary')) {
                showDialog($macro,$macro.data('lic-key'), $macro.data('lic-summary'),$macro.data('lic-status'));
                return;
            }

            var params = $macro.attr('data-macro-parameters') \vert{}\vert{}$macro.attr('data-macro-default-parameter') || '';
            var keyMatch = params.match(/([A-Z0-9]+-\d+)/i);
            if (!keyMatch) return;

            var issueKey = keyMatch[1].toUpperCase();

            var restUrl = AJS.contextPath() + 
                          '/rest/jira-integration/1.0/issues?issueKey=' + encodeURIComponent(issueKey) + 
                          '&fields=summary,status';

            $.ajax({
                url: restUrl,
                type: 'GET',
                dataType: 'json',
                success: function (data) {
                    var issue = (data && data.issues && data.issues[0]) || (Array.isArray(data) && data[0]);
                    if (issue) {
                        var summaryText = issue.summary || (issue.fields && issue.fields.summary);
                        var statusObj = issue.status || (issue.fields && issue.fields.status);
                        var statusName = statusObj ? (statusObj.name || statusObj) : '';

                        if (summaryText) {
                            $macro.data('lic-key', issueKey);
                            $macro.data('lic-summary', summaryText);$macro.data('lic-status', statusName);

                            showDialog($macro, issueKey, summaryText, statusName);
                        }
                    }
                }
            });
        });

        $iframeDoc.on('mouseout.licHover', '*[data-macro-name="jira"]', function () {
            $dialog.hide();
        });

        function showDialog($macro, key, summary, status) {
            var html = '<div class="lic-dialog-header">' +
                         '<span class="lic-dialog-key">' + key + '</span>' +
                         (status ? '<span class="lic-dialog-status">' + status + '</span>' : '') +
                       '</div>' +
                       '<div class="lic-dialog-body">' + summary + '</div>';

            $dialog.html(html);

            var rect = getMacroGlobalRect(editor, $macro);
            var dialogHeight = $dialog.outerHeight();

            var topPos = rect.top - dialogHeight - 8;
            if (topPos < $(window).scrollTop() + 10) {
                topPos = rect.top + rect.height + 8;
            }

            $dialog.css({
                top: Math.max(5, topPos) + 'px',
                left: Math.max(5, rect.left) + 'px',
                display: 'block'
            });
        }
    }

    function waitForEditorAndInit() {
        var editor = findConfluenceEditor();
        if (editor) {
            initJiraMacroHover(editor);
            return;
        }

        var attempts = 0;
        var timer = setInterval(function () {
            attempts++;
            var ed = findConfluenceEditor();
            if (ed) {
                clearInterval(timer);
                initJiraMacroHover(ed);
            } else if (attempts > 60) {
                clearInterval(timer);
            }
        }, 200);
    }

    if (window.AJS && typeof AJS.toInit === 'function') {
        AJS.toInit(waitForEditorAndInit);
    } else {
        $(document).ready(waitForEditorAndInit);
    }
})(AJS.$ || jQuery);