/**
* Fetches suggestions for a control from some source location.
*
* @interface DescriptorFetcher
*/
/**
* Sets up a request
*
* @function
* @name DescriptorFetcher#execute
* @param {Function} query - lazily evaluated value of input field.
* @param {Boolean} force - Piss off all buffers etc. Make request now!
* @return {jQuery.Deferred}
*/
define('jira/ajs/select/suggestions/suggest-helper', ['jira/ajs/list/group-descriptor', 'jira/ajs/list/item-descriptor', 'jira/ajs/select/fetchers/mixed-descriptor-fetcher', 'jira/ajs/select/fetchers/ajax-descriptor-fetcher', 'jira/ajs/select/fetchers/func-descriptor-fetcher', 'jira/ajs/select/fetchers/static-descriptor-fetcher', 'underscore'], function (GroupDescriptor, ItemDescriptor, MixedDescriptorFetcher, AjaxDescriptorFetcher, FuncDescriptorFetcher, StaticDescriptorFetcher, _) {
'use strict';
/**
* A utility object to manipulate/create suggestions
* @name SuggestHelper
* @exports jira/ajs/select/suggestions/suggest-helper
*/
return {
/**
* Factory method to create descriptor fetcher based on user optiosn
*
* @param options
* @param {SelectModel} model
*/
createDescriptorFetcher: function createDescriptorFetcher(options, model) {
if (options.ajaxOptions && options.ajaxOptions.url) {
if (model && options.content === "mixed") {
return new MixedDescriptorFetcher(options, model);
} else {
return new AjaxDescriptorFetcher(options.ajaxOptions);
}
} else if (options.suggestions) {
return new FuncDescriptorFetcher(options);
} else if (model) {
return new StaticDescriptorFetcher(options, model);
}
},
/**
* Extract all item descriptors within an array of group descriptors.
*
* @param descriptors {GroupDescriptor[]} The group descriptors.
* @return {ItemDescriptor[]} All item descriptors within.
*/
extractItems: function extractItems(descriptors) {
return _.flatten(_.map(descriptors, function (descriptor) {
if (descriptor instanceof GroupDescriptor) {
return descriptor.items();
} else {
return [descriptor];
}
}));
},
/**
* Creates a descriptor group that mirrors the inputted query
* @param {String} query
* @param {String} label
* @param {Boolean} uppercaseValue
* @return {GroupDescriptor}
*/
mirrorQuery: function mirrorQuery(query, label, uppercaseValue) {
var value = uppercaseValue ? query.toUpperCase() : query;
return new GroupDescriptor({
label: "user inputted option",
showLabel: false,
replace: true
}).addItem(new ItemDescriptor({
value: value,
label: value,
labelSuffix: " (" + label + ")",
title: value,
allowDuplicate: false,
noExactMatch: true // this item doesn't count as an exact query match for selthis.ection purposes
}));
},
/**
* Does the item descriptor match any of the selected values
* @param {ItemDescriptor} itemDescriptor
* @param {String[]} selectedVals
* @return {Boolean}
*/
isSelected: function isSelected(itemDescriptor, selectedVals) {
return _.any(selectedVals, function (descriptor) {
return itemDescriptor.value() === descriptor.value();
});
},
/**
* Removes duplicate descriptors
*
* @param descriptors
* @param vals
* @return {Array}
*/
removeDuplicates: function removeDuplicates(descriptors, vals) {
vals = vals || [];
return _.filter(descriptors, _.bind(function (descriptor) {
if (descriptor instanceof GroupDescriptor) {
descriptor.items(this.removeDuplicates(descriptor.items(), vals));
return true;
} else if (!_.include(vals, descriptor.value())) {
if (descriptor.value()) {
vals.push(descriptor.value());
}
return true;
}
}, this));
},
/**
* Loop over all descriptors and remove descriptors that match selected vals. Usually if the user has already
* selected a suggestion, we don't want to show it.
* @param {GroupDescriptor[] | ItemDescriptor[]} descriptors
* @param {String[]} selectedValues
* @return {GroupDescriptor[] | ItemDescriptor[]} descriptors
* @private
*/
removeSelected: function removeSelected(descriptors, selectedValues) {
return _.filter(descriptors, _.bind(function (descriptor) {
if (descriptor instanceof ItemDescriptor && this.isSelected(descriptor, selectedValues)) {
return false;
}
if (descriptor instanceof GroupDescriptor) {
descriptor.items(this.removeSelected(descriptor.items(), selectedValues));
}
return true;
}, this));
}
};
});
define('jira/ajs/select/suggestions/default-suggest-handler', ['jira/jquery/deferred', 'jira/lib/class', 'jira/ajs/select/suggestions/suggest-helper', 'underscore'], function (Deferred, Class, SuggestHelper, _) {
'use strict';
/**
* A default suggestion handler. Used for autocomplete without a backing <select>
* @class SuggestHandler
* @class DefaultSuggestHandler
* @exports jira/ajs/select/suggestions/default-suggest-handler
*/
return Class.extend({
/**
* @param {Object} options
* @constructs
*/
init: function init(options) {
this.options = options;
this.descriptorFetcher = SuggestHelper.createDescriptorFetcher(options);
},
/**
* Check if we should mirror input as a suggestion
* @param {String} query
* @return {Boolean}
*/
validateMirroring: function validateMirroring(query) {
return this.options.userEnteredOptionsMsg && query.length > 0;
},
/**
* Applies default formatting
*
* @param {Array} descriptors
* @param {String} query
* @return {*}
*/
formatSuggestions: function formatSuggestions(descriptors, query) {
if (this.validateMirroring(query)) {
descriptors.push(SuggestHelper.mirrorQuery(query, this.options.userEnteredOptionsMsg, this.options.uppercaseUserEnteredOnSelect));
}
return descriptors;
},
/**
* Requests descriptors then formats them
* @param {String} query
* @param {Boolean} force
* @return {*}
*/
execute: function execute(query, force) {
var deferred = new Deferred();
var fetcherDef = this.descriptorFetcher.execute(query, force).done(_.bind(function (descriptors) {
if (descriptors) {
descriptors = this.formatSuggestions(descriptors, query);
}
deferred.resolve(descriptors, query);
}, this));
deferred.fail(function () {
fetcherDef.reject();
});
return deferred;
}
});
});
define('jira/ajs/select/suggestions/select-suggest-handler', ['jira/ajs/select/suggestions/default-suggest-handler', 'jira/ajs/select/suggestions/suggest-helper'], function (DefaultSuggestHandler, SuggestHelper) {
'use strict';
/**
* A suggestion handler that removes suggestions that have already been selected in <select>
* @class SelectSuggestHandler
* @extends DefaultSuggestHandler
* @exports jira/ajs/select/suggestions/select-suggest-handler
*/
return DefaultSuggestHandler.extend({
/**
* @param {Object} options
* @param {SelectModel} model
* @constructs
*/
init: function init(options, model) {
this.descriptorFetcher = SuggestHelper.createDescriptorFetcher(options, model);
this.options = options;
this.model = model;
},
/**
* Formats suggestions removing already selected descriptors
* @param descriptors
* @param query
* @return {GroupDescriptor[]}*/
formatSuggestions: function formatSuggestions(descriptors, query) {
var suggestions = this._super(descriptors, query);
var selectedDescriptors = this.model.getDisplayableSelectedDescriptors();
if (this.options.removeDuplicates) {
suggestions = SuggestHelper.removeDuplicates(descriptors);
}
return SuggestHelper.removeSelected(suggestions, selectedDescriptors);
}
});
});
define('jira/ajs/select/suggestions/assignee-suggest-handler', ['jira/util/formatter', 'jira/ajs/select/suggestions/select-suggest-handler'], function (formatter, SelectSuggestHandler) {
'use strict';
/**
* Special handler for assignee picker that appends some footer text prompting user to start typing for more options.
* @class AssigneeSuggestHandler
* @extends SelectSuggestHandler
* @exports jira/ajs/select/suggestions/assignee-suggest-handler
*/
return SelectSuggestHandler.extend({
/**
* Formats suggestions removing already selected descriptors
* @param descriptors
* @param query
* @return {GroupDescriptor[]}
*/
formatSuggestions: function formatSuggestions(descriptors, query) {
var groupDescriptors = this._super(descriptors, query);
if (query.length === 0) {
groupDescriptors[0].footerText(formatter.I18n.getText("user.picker.ajax.short.desc"));
}
return groupDescriptors;
}
});
});
define('jira/ajs/select/suggestions/checkbox-multi-select-suggest-handler', ['jira/util/formatter', 'jira/ajs/select/suggestions/select-suggest-handler', 'jira/ajs/select/suggestions/suggest-helper', 'jira/ajs/list/group-descriptor'], function (formatter, SelectSuggestHandler, SuggestHelper, GroupDescriptor) {
'use strict';
/**
* A suggestion handler that without a query, shows selected items at the top followed by unselected items in their groups.
* When querying selected and unselected items are munged together and sorted in alphabetical order.
* @class CheckboxMultiSelectSuggestHandler
* @extends SelectSuggestHandler
* @exports jira/ajs/select/suggestions/checkbox-multi-select-suggest-handler
*/
return SelectSuggestHandler.extend({
/**
* Creates html string for clear all
* @return {String}
*/
createClearAll: function createClearAll() {
return "
" + formatter.I18n.getText("jira.ajax.autocomplete.clear.all") + "";
},
/**
* Formats descriptors for display in checkbox multiselect
*
* @param descriptors
* @param query
* @return {Array} formatted descriptors
*/
formatSuggestions: function formatSuggestions(descriptors, query) {
var selectedItems = SuggestHelper.removeDuplicates(this.model.getDisplayableSelectedDescriptors());
var selectedGroup = new GroupDescriptor({
styleClass: "selected-group",
items: selectedItems,
actionBarHtml: selectedItems.length > 1 ? this.createClearAll() : null
});
descriptors.splice(0, 0, selectedGroup);
if (query.length > 0) {
descriptors = SuggestHelper.removeDuplicates(descriptors);
// Extract all items from the descriptors and sort them by label.
var items = SuggestHelper.extractItems(descriptors).sort(function (a, b) {
a = a.label().toLowerCase();
b = b.label().toLowerCase();
return a.localeCompare(b);
});
descriptors = [new GroupDescriptor({ items: items })];
}
return descriptors;
}
});
});
define('jira/ajs/select/suggestions/user-list-suggest-handler', ['jira/ajs/select/suggestions/select-suggest-handler'], function (SelectSuggestHandler) {
'use strict';
/**
* Special handler for share dialog pickers.
* @class UserListSuggestHandler
* @extends SelectSuggestHandler
* @exports jira/ajs/select/suggestions/user-list-suggest-handler
*/
return SelectSuggestHandler.extend({
/**
* Tests valid email address
*/
emailExpression: /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,
/**
* Only mirror user input if it is a valid email address
* @param {String} query
* @return {Boolean}
*/
validateMirroring: function validateMirroring(query) {
return this.options.freeEmailInput && query.length > 0 && this.emailExpression.test(query);
}
});
});
define('jira/ajs/select/suggestions/only-new-items-suggest-handler', ['jira/ajs/select/suggestions/select-suggest-handler', 'underscore'], function (SelectSuggestHandler, _) {
'use strict';
/**
* Special handler that will only allow new items to be mirrored.
* An item is considered new if it does not match the label of any of the displayable options.
* Matching is case-insensitive.
*
* @class OnlyNewItemsSuggestHandler
* @extends SelectSuggestHandler
* @exports jira/ajs/select/suggestions/only-new-items-suggest-handler
*/
return SelectSuggestHandler.extend({
/**
* Only mirror user input if it doesn't exist yet in the list of options based on label match.
* @param {String} query
* @return {Boolean}
*/
validateMirroring: function validateMirroring(query) {
var allowMirroring = this._super(query);
if (allowMirroring) {
var lowerCaseQuery = query.toLowerCase();
var allExistingDescriptors = this.model.getDisplayableSelectedDescriptors().concat(this.model.getDisplayableUnSelectedDescriptors());
var existingItem = _.some(allExistingDescriptors, function (descriptor) {
var label = descriptor.label();
return label && label.toLowerCase() === lowerCaseQuery;
});
return !existingItem;
}
return false;
}
});
});
define('jira/ajs/select/fetchers/static-descriptor-fetcher', ['jira/jquery/deferred', 'jira/lib/class'], function (Deferred, Class) {
'use strict';
/**
* Gets unselected <option>s from <select> as suggestions
* @class StaticDescriptorFetcher
* @extends Class
* @implements DescriptorFetcher
* @exports jira/ajs/select/fetchers/static-descriptor-fetcher
*/
return Class.extend({
/**
* @param {Object} options - empty in this case
* @param {SelectModel} model - a wrapper around <select> element
* @constructs
*/
init: function init(options, model) {
this.model = model;
this.model.$element.data("static-suggestions", true);
},
/**
* @return {jQuery.Deferred}
*/
execute: function execute(query) {
var deferred = new Deferred();
deferred.resolve(this.model.getUnSelectedDescriptors(), query);
return deferred;
}
});
});
define('jira/ajs/select/fetchers/ajax-descriptor-fetcher', ['jira/util/logger', 'jira/jquery/deferred', 'jira/lib/class', 'jira/ajs/ajax/smart-ajax', 'underscore'], function (logger, Deferred, Class, SmartAjax, _) {
'use strict';
/**
* Retrieves json from server and converts it into descriptors using formatSuggestions function supplied by user.
* @class AjaxDescriptorFetcher
* @extends Class
* @implements DescriptorFetcher
* @exports jira/ajs/select/fetchers/ajax-descriptor-fetcher
*/
return Class.extend({
/**
* @param options
* @constructs
*/
init: function init(options) {
this.options = _.extend({
keyInputPeriod: 75, // Wait this long between key strokes before going to server
minQueryLength: 1, // Need these many characters before we go to server
data: {},
dataType: "json"
}, options);
},
// Actually make the request and notify those interested
makeRequest: function makeRequest(deferred, ajaxOptions, query) {
ajaxOptions.complete = _.bind(function () {
this.outstandingRequest = null;
logger.trace("jira.suggestionhandler.done");
}, this);
ajaxOptions.success = _.bind(function (data) {
if (ajaxOptions.query) {
deferred.resolve(ajaxOptions.formatResponse(data, query));
} else {
this.lastResponse = ajaxOptions.formatResponse(data, query);
deferred.resolve(this.lastResponse);
}
}, this);
var originalError = ajaxOptions.error;
ajaxOptions.error = function (xhr, textStatus, msg, smartAjaxResult) {
if (!smartAjaxResult.aborted) {
if (originalError) {
originalError.apply(this, arguments);
} else {
alert(SmartAjax.buildSimpleErrorContent(smartAjaxResult, { alert: true }));
}
deferred.resolve();
} else {
if (smartAjaxResult.statusText === 'timeout') {
if (originalError) {
originalError.apply(this, arguments);
} else {
alert(SmartAjax.buildSimpleErrorContent(smartAjaxResult, { alert: true }));
}
deferred.resolve();
}
}
};
this.outstandingRequest = SmartAjax.makeRequest(ajaxOptions); // issue requestcle
},
/**
* Prepare the data and prevent throttling of server
* @param {jQuery.Deferred} deferred
* @param {Object} ajaxOptions - standard jQuery ajax options
* @param {String} query - in most cases this is the user input
* @param {Boolean} force - ignore request buffers. I want my request dispatched NOW.
*/
incubateRequest: function incubateRequest(deferred, ajaxOptions, query, force) {
clearTimeout(this.queuedRequest); // cancel any queued requests
if (force && this.outstandingRequest) {
this.outstandingRequest.abort();
this.outstandingRequest = null;
}
if (!ajaxOptions.query && this.lastResponse) {
deferred.resolve(this.lastResponse);
} else if (!this.outstandingRequest) {
if (typeof ajaxOptions.data === 'function') {
ajaxOptions.data = ajaxOptions.data(query);
} else {
ajaxOptions.data.query = query;
}
if (typeof ajaxOptions.url === 'function') {
ajaxOptions.url = ajaxOptions.url();
}
if (query.length >= parseInt(ajaxOptions.minQueryLength, 10) || force) {
this.makeRequest(deferred, ajaxOptions, query);
} else {
deferred.resolve();
}
} else {
this.queuedRequest = setTimeout(_.bind(function () {
this.incubateRequest(deferred, ajaxOptions, query, true);
}, this), ajaxOptions.keyInputPeriod);
}
return deferred;
},
/**
* Sets up a request
* @param {Function} query - lazily evaluated value of input field.
* @param {Boolean} force - Piss off all buffers etc. Make request now!
* @return {jQuery.Deferred}
*/
execute: function execute(query, force) {
var deferred = new Deferred();
deferred.fail(_.bind(function () {
clearTimeout(this.queuedRequest);
if (this.outstandingRequest) {
this.outstandingRequest.abort();
}
}, this));
this.incubateRequest(deferred, _.extend({}, this.options), query, force);
return deferred;
}
});
});
define('jira/ajs/select/fetchers/mixed-descriptor-fetcher', ['jira/jquery/deferred', 'jira/lib/class', 'jira/ajs/select/fetchers/ajax-descriptor-fetcher', 'underscore'], function (Deferred, Class, AjaxDescriptorFetcher, _) {
'use strict';
/**
* Gets suggestions from unselected <option>s in <select> as well as going to the server upon character for more
* results on input.
*
* @class MixedDescriptorFetcher
* @extends Class
* @implements DescriptorFetcher
* @exports jira/ajs/select/fetchers/mixed-descriptor-fetcher
*/
return Class.extend({
/**
* @param {Object} options - jQuery ajax options object. With additional:
* @param {function} options.formatResponse - function for creating descriptors out of server response
* @param {number} options.minQueryLength - min input length before a request is made
* @param {Object} options.ajaxOptions
* @param {SelectModel} model - a wrapper around <select> element
* @constructs
*/
init: function init(options, model) {
this.ajaxFetcher = new AjaxDescriptorFetcher(options.ajaxOptions);
this.options = options;
this.model = model;
},
/**
* @param query
* @param force
* @return {jQuery.Deferred}
*/
execute: function execute(query, force) {
var deferred = new Deferred();
// This needs to come after the return statement...
var minQueryLength = this.options.ajaxOptions.minQueryLength;
minQueryLength = typeof minQueryLength === "number" && isFinite(minQueryLength) ? minQueryLength : 1;
if (query.length >= minQueryLength) {
var ajaxDeferred = this.ajaxFetcher.execute(query, force).done(_.bind(function (suggestions) {
var descriptors;
if (this.options.suggestionAtTop) {
descriptors = [].concat(this.model.getAllDescriptors(), suggestions);
} else {
descriptors = [].concat(suggestions, this.model.getAllDescriptors());
}
deferred.resolve(descriptors, query);
}, this));
deferred.fail(function () {
ajaxDeferred.reject();
});
} else {
deferred.resolve(this.model.getUnSelectedDescriptors(), query);
}
return deferred;
}
});
});
define('jira/ajs/select/fetchers/func-descriptor-fetcher', ['jira/jquery/deferred', 'jira/lib/class'], function (Deferred, Class) {
'use strict';
/**
* A single fetcher that will just return the result of calling supplied function
*
* @class FuncDescriptorFetcher
* @extends Class
* @implements DescriptorFetcher
* @exports jira/ajs/select/fetchers/func-descriptor-fetcher
*/
return Class.extend({
/**
* @param {Object} options
* @constructs
*/
init: function init(options) {
this.options = options;
},
/**
* Gets result of function
* @param query
*/
execute: function execute(query) {
var deferred = new Deferred();
deferred.resolve(this.options.suggestions(query), query);
return deferred;
}
});
});
AJS.namespace('AJS.SuggestHelper', null, require('jira/ajs/select/suggestions/suggest-helper'));
AJS.namespace('AJS.DefaultSuggestHandler', null, require('jira/ajs/select/suggestions/default-suggest-handler'));
AJS.namespace('AJS.SelectSuggestHandler', null, require('jira/ajs/select/suggestions/select-suggest-handler'));
AJS.namespace('AJS.OnlyNewItemsSuggestHandler', null, require('jira/ajs/select/suggestions/only-new-items-suggest-handler'));
AJS.namespace('AJS.CheckboxMultiSelectSuggestHandler', null, require('jira/ajs/select/suggestions/checkbox-multi-select-suggest-handler'));
AJS.namespace('JIRA.AssigneeSuggestHandler', null, require('jira/ajs/select/suggestions/assignee-suggest-handler'));
AJS.namespace('AJS.UserListSuggestHandler', null, require('jira/ajs/select/suggestions/user-list-suggest-handler'));
AJS.namespace('AJS.StaticDescriptorFetcher', null, require('jira/ajs/select/fetchers/static-descriptor-fetcher'));
AJS.namespace('AJS.AjaxDescriptorFetcher', null, require('jira/ajs/select/fetchers/ajax-descriptor-fetcher'));
AJS.namespace('AJS.MixedDescriptorFetcher', null, require('jira/ajs/select/fetchers/mixed-descriptor-fetcher'));
AJS.namespace('AJS.FuncDescriptorFetcher', null, require('jira/ajs/select/fetchers/func-descriptor-fetcher'));