Last updated Oct 9, 2024

Extending inline edit for JIRA plugins

Available:

JIRA 5.1 and later.

Overview and purpose

The page provides information for JIRA plugin developers who wish to interact with JIRA's inline edit features that were introduced in JIRA 5.1.

The information on this page is typically most useful to JIRA plugin developers who wish to expose anything (such as custom fields or web panels) on JIRA's 'view issue' page.

Working with the NEW_CONTENT_ADDED event

With inline edit enabled, document content changes frequently and there are several situations where plugins may need to reconfigure elements (e.g. bind event handlers, create elements). This should be performed in response to the JIRA.Events.NEW_CONTENT_ADDED event (introduced in JIRA 5.0), which is triggered whenever new HTML is inserted into the document. The event handler is passed three arguments:

  • e -- the event object,
  • context -- the context in which the content was added (may be null or undefined) and
  • reason -- the reason the event was triggered (new in JIRA 5.1 for inline edit).

The following table indicates the available reason arguments and their associated contexts:

Reason

Description

1
2
JIRA
  .CONTENT_ADDED_REASON
  .inlineEditStarted

The user started inline editing a field -- its 'view' HTML has been replaced with its 'edit' HTML.
The context is the element containing the field and the cancel/save buttons.

1
2
JIRA
  .CONTENT_ADDED_REASON
  .pageLoad

The view issue page finished loading.
The context is the document.

1
2
JIRA
  .CONTENT_ADDED_REASON
  .panelRefreshed

A panel on the view issue page (e.g. details, description) was refreshed after an inline edit -- new HTML has been inserted into the document.
The context is the panel element.

1
2
JIRA
  .CONENT_ADDED_REASON
  .issueTableRefreshed
(List View only) The table of results has been updated by a direct action from the user (i.e. click on the Refresh icon), because the user requested a new search/filter or because the user switched to a different results page. The context is the element containing the new results table.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .issueTableRowRefreshed
(List View only) A row from the results table has been updated with new information about the issue. The context is the updated table row.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .criteriaPanelRefreshed
The user opened a dropdown menu from a criteria in the Basic Search. The context is the content of the dropdown menu.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .filterPanelOpened
The Filters panel has been opened. The context is the container of the Filters panel.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .layoutSwitcherReady
The Layout switcher has been rendered in the page. The context is the container of the button.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .returnToSearch
(List View only) The user has returned to the search from an issue. The context is the Search header.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .shareDialogOpened
The Share dialog is opened. The context is the dialog.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .filtersSearchRefreshed
In the Search Filters section, the results table has been refreshed. The context is the whole section.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .tabUpdated
When the user clicks on a tab in pages with vertical tabs navigation. The context is the new section.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .dialogReady
When any dialog is ready to be rendered. The context is the content of the dialog.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .componentsTableReady
On the Components Administration page, the results table of current components is ready to be rendered. The context is the results table.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .workflowReady
On the Workflows Administration page, a workflow is ready to be rendered. The context is the workflow container.
1
2
JIRA
  .CONTENT_ADDED_REASON
  .workflowHeaderReady
On the Workflows Administration page, the header of the page is ready to be rendered. The context is the container of the header.

Please Note:

  • The reason argument will not be passed in JIRA 5.0.x versions.
  • Ensure that you scope jQueryselectors to the provided context to avoid double binding -- for example:
1
2
// The following code snippet will match elements elsewhere on the page (i.e. bad!):
var elements = AJS.$(".my-element");
  
// The following code snippet will match only elements in $context (i.e. good!):
var elements = $context.find(".my-element");
  • It is recommended to always check the 'reason' argument.
  • It is recommended to assume that NEW_CONTENT_ADDED could be triggered at any moment and multiple times, depending on the actions performed by the user.

The following example demonstrates how you might configure custom fields and a web panel:

1
2
AJS.$(function() {
    JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function(e, context, reason) {
        var $context = AJS.$(context);
 
        // Find our web panel. Handles the pageLoad and panelRefreshed reasons.
        var $webPanel = $context.find("*").andSelf().filter("#my-web-panel");
        if ($webPanel.length > 0) {
            // ...
        }
 
        // Find our custom fields. There may be multiple!
        $context.find(".my-custom-field").each(function() {
            var $customField = AJS.$(this);
            // ...
        });
    });
});

Custom fields and 'save on blur'

Save on blur is the automatic saving of inline edits made to a field when it loses focus (i.e. when the user clicks elsewhere on the page or tabs away from the field). All of JIRA's system and custom fields opt in to save on blur. However, save on blur is disabled by default for third party custom fields. Why? Custom fields generally display all of their content inside the inline edit boundaries, but sometimes add extra HTML in different areas of the DOM -- e.g. inline layers or popup windows. JIRA cannot detect whether or not a piece of HTML "belongs" to a custom field editor and as such, we require custom fields provided by third party plugins to let JIRA know when they have been blurred.

To enable save on blur for a custom field, it must be registered in JIRA.Components.IssueEditor.InlineEditUtils.BlurTriggerMapping.custom which maps custom field types to blur triggers -- i.e. functions that determine when a field has lost focus and should be saved.

Opting in to the default blur trigger

If your custom field's edit HTML is entirely contained within the inline editor boundaries (i.e. it does not create inline layers, popup windows or any external DOM content), use JIRA.Issues.InlineEdit.BlurTriggers.Default. This fires the "blur" event when focus leaves the inline editor boundaries.

The following example shows how you might register a custom field to use the default blur trigger:

atlassian-plugin.xml (fragment)

1
2
<customfield-type key="my-custom-field"
    class="com.example.customfields.MyCustomField">
        ...
</customfield-type>

my-custom-field.js

1
2
AJS.$(function() {
    // The field need only be registered on page load. This can be achieved
    // using the `reason` argument of the NEW_CONTENT_ADDED event (new in JIRA 5.1).
    JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function(e, context, reason) {
        if (reason == JIRA.CONTENT_ADDED_REASON.pageLoad) {
            var customFieldTypeKey = "my-custom-field"; // From atlassian-plugin.xml.
            var defaultBlurTrigger = JIRA.Components.IssueEditor.InlineEditUtils.BlurTriggers.Default;
            JIRA.Components.IssueEditor.InlineEditUtils.BlurTriggerMapping.custom[fieldType] = defaultBlurTrigger;
        }
    });
});

Writing a custom blur trigger

The default blur trigger (used in the example above) will work well for most fields, but those that make use of popups, windows or elements located elsewhere in the DOM will require a custom blur trigger. A blur trigger is a function that determines when a field has lost focus and should be saved. The following is the simplest possible implementation of a blur trigger:

1
2
var SimpleBlurTrigger = function(fieldId, $container) {
    // Announce that the field described by fieldId has blurred.
    JIRA.trigger(JIRA.Events.INLINE_EDIT_BLURRED, [fieldId]);
};
 
// SimpleBlurTrigger will be called when we start inline editing a field whose type is "my-custom-field-type".
JIRA.Components.IssueEditor.InlineEditUtils.BlurTriggerMapping.custom["my-custom-field-type"] = SimpleBlurTrigger;

This blur trigger will announce that the field has blurred immediately after entering inline edit by triggering the JIRA.Events.INLINE_EDIT_BLURRED event, passing the field's ID; while not very functional, this example demonstrates the basic structure of a blur trigger. The function is passed two arguments:

  • fieldId -- the ID of the field that entered inline edit; and
  • $container -- the element containing the inline editor--this contains your custom field's HTML as well as the save and cancel buttons provided by the inline edit framework.

Using these arguments, the blur trigger must bind to relevant events to determine when the field has lost focus. Situations that should be considered include:

  • the input element (i.e. field) itself losing focus,
  • elements located elsewhere in the DOM losing focus (e.g. inline dialogs), and
  • the save/cancel buttons losing focus.

Example

The following example is for a custom field that generates an external layer (in this case a warning) and attaches a custom save on blur trigger.

First let's listen to JIRA.Events.NEW_CONTENT_ADDED and add a warning whenever a user starts editing the field.

1
2
AJS.namespace("WarningCustomField");
 
AJS.$(function() {
    JIRA.bind(JIRA.Events.NEW_CONTENT_ADDED, function(e, context, reason) {
        var containsWarningField = AJS.$("#warningField", context).length > 0;
        var isInlineEditStarted = reason === JIRA.CONTENT_ADDED_REASON.inlineEditStarted;
        if (containsWarningField && isInlineEditStarted) {
            WarningCustomField.$container = JIRA.Messages.showWarningMsg("Danger, Will Robinson!");
        }
    });
});

Now, we want to trigger save-on-blur on this field, but not if the user clicks on the above warning message:

1
2
var WarningBlurTrigger = function(fieldId, $container) {
    var focusables = ':input, a[href], [tabindex]'; // These are the jQuery selectors of the elements we're interested in
    var timeout;
    var eventsMap = {
        blur: function() {
            if (timeout) clearTimeout(timeout);
            timeout = setTimeout(triggerIfBlurred, JIRA.Issues.InlineEdit.BLUR_FOCUS_TIMEOUT);
        }
    };
    function makeFocusable($el) {
        $el.attr('tabindex', 1) // Adding a tab index makes the element focusable
            .bind(eventsMap) // Bind to container
            .delegate(focusables, eventsMap); // Bind to focusable elements in the container
    }
    function triggerIfBlurred() {
        var warning = WarningCustomField.$container;
        if (!hasFocus($containerChrome) && !hasFocus(warning)) {
            if (warning) {
                // Unbind event handlers from the toolbar unless it is getting destroyed later
                warning.unbind(eventsMap).undelegate(focusables, eventsMap);
                // Also hide the message!
                warning.remove();
            }
            // Trigger the INLINE_EDIT_BLURRED event!
            JIRA.trigger(JIRA.Events.INLINE_EDIT_BLURRED, [fieldId]);
        }
    }
    function hasFocus($element) {
        if (!$element) {
            return false;
        }
        var activeElement = document.activeElement;
        return $element.find(activeElement).length > 0 || $element.filter(activeElement).length > 0;
    }
    // To make normal inline edit blurring work, we must include the save and cancel buttons:
    var $containerChrome = $container.nextAll(".save-options").andSelf();
    makeFocusable($containerChrome);
    // Add the warning message we created on inline edit start
    if (WarningCustomField.$container) {
        makeFocusable(WarningCustomField.$container);
    }
};
AJS.$(function() {
    // Register the custom blur trigger
    JIRA.Components.IssueEditor.InlineEditUtils.BlurTriggerMapping.custom.warningField = WarningBlurTrigger;
});

Rate this page: