Files
PyRIGS/RIGS/static/js/ajax-bootstrap-select.js
2017-09-22 16:33:04 +01:00

1613 lines
53 KiB
JavaScript

/*!
* Ajax Bootstrap Select
*
* Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON.
*
* @version 1.4.1
* @author Adam Heim - https://github.com/truckingsim
* @link https://github.com/truckingsim/Ajax-Bootstrap-Select
* @copyright 2017 Adam Heim
* @license Released under the MIT license.
*
* Contributors:
* Mark Carver - https://github.com/markcarver
*
* Last build: 2017-07-21 1:08:54 PM GMT-0400
*/
!(function ($, window) {
/**
* @class AjaxBootstrapSelect
*
* @param {jQuery|HTMLElement} element
* The select element this plugin is to affect.
* @param {Object} [options={}]
* The options used to affect the desired functionality of this plugin.
*
* @return {AjaxBootstrapSelect|null}
* A new instance of this class or null if unable to instantiate.
*/
var AjaxBootstrapSelect = function (element, options) {
var i, l, plugin = this;
options = options || {};
/**
* The select element this plugin is being attached to.
* @type {jQuery}
*/
this.$element = $(element);
/**
* The merged default and passed options.
* @type {Object}
*/
this.options = $.extend(true, {}, $.fn.ajaxSelectPicker.defaults, options);
/**
* Used for logging error messages.
* @type {Number}
*/
this.LOG_ERROR = 1;
/**
* Used for logging warning messages.
* @type {Number}
*/
this.LOG_WARNING = 2;
/**
* Used for logging informational messages.
* @type {Number}
*/
this.LOG_INFO = 3;
/**
* Used for logging debug messages.
* @type {Number}
*/
this.LOG_DEBUG = 4;
/**
* The jqXHR object of the last request, false if there was none.
* @type {jqXHR|Boolean}
*/
this.lastRequest = false;
/**
* The previous query that was requested.
* @type {String}
*/
this.previousQuery = '';
/**
* The current query being requested.
* @type {String}
*/
this.query = '';
/**
* The jqXHR object of the current request, false if there is none.
* @type {jqXHR|Boolean}
*/
this.request = false;
// Maps deprecated options to new ones between releases.
var deprecatedOptionsMap = [
// @todo Remove these options in next minor release.
{
from: 'ajaxResultsPreHook',
to: 'preprocessData'
},
{
from: 'ajaxSearchUrl',
to: {
ajax: {
url: '{{{value}}}'
}
}
},
{
from: 'ajaxOptions',
to: 'ajax'
},
{
from: 'debug',
to: function (map) {
var _options = {};
_options.log = Boolean(plugin.options[map.from]) ? plugin.LOG_DEBUG : 0;
plugin.options = $.extend(true, {}, plugin.options, _options);
delete plugin.options[map.from];
plugin.log(plugin.LOG_WARNING, 'Deprecated option "' + map.from + '". Update code to use:', _options);
}
},
{
from: 'mixWithCurrents',
to: 'preserveSelected'
},
{
from: 'placeHolderOption',
to: {
locale: {
emptyTitle: '{{{value}}}'
}
}
}
];
if (deprecatedOptionsMap.length) {
$.map(deprecatedOptionsMap, function (map) {
// Depreciated option detected.
if (plugin.options[map.from]) {
// Map with an object. Use "{{{value}}}" anywhere in the object to
// replace it with the passed value.
if ($.isPlainObject(map.to)) {
plugin.replaceValue(map.to, '{{{value}}}', plugin.options[map.from]);
plugin.options = $.extend(true, {}, plugin.options, map.to);
plugin.log(plugin.LOG_WARNING, 'Deprecated option "' + map.from + '". Update code to use:', map.to);
delete plugin.options[map.from];
}
// Map with a function. Functions are silos. They are responsible
// for deleting the original option and displaying debug info.
else if ($.isFunction(map.to)) {
map.to.apply(plugin, [map]);
}
// Map normally.
else {
var _options = {};
_options[map.to] = plugin.options[map.from];
plugin.options = $.extend(true, {}, plugin.options, _options);
plugin.log(plugin.LOG_WARNING, 'Deprecated option "' + map.from + '". Update code to use:', _options);
delete plugin.options[map.from];
}
}
});
}
// Retrieve the element data attributes.
var data = this.$element.data();
// @todo Deprecated. Remove this in the next minor release.
if (data['searchUrl']) {
plugin.log(plugin.LOG_WARNING, 'Deprecated attribute name: "data-search-url". Update markup to use: \' data-abs-ajax-url="' + data['searchUrl'] + '" \'');
this.options.ajax.url = data['searchUrl'];
}
// Helper functions.
var matchToLowerCase = function (match, p1) { return p1.toLowerCase(); };
var expandObject = function (keys, value, obj) {
var k = [].concat(keys), l = k.length, o = obj || {};
if (l) { var key = k.shift(); o[key] = expandObject(k, value, o[key]); }
return l ? o : value;
};
// Filter out only the data attributes prefixed with 'data-abs-'.
var dataKeys = Object.keys(data).filter(/./.test.bind(new RegExp('^abs[A-Z]')));
// Map the data attributes to their respective place in the options object.
if (dataKeys.length) {
// Object containing the data attribute options.
var dataOptions = {};
var flattenedOptions = ['locale'];
for (i = 0, l = dataKeys.length; i < l; i++) {
var name = dataKeys[i].replace(/^abs([A-Z])/, matchToLowerCase).replace(/([A-Z])/g, '-$1').toLowerCase();
var keys = name.split('-');
// Certain options should be flattened to a single object
// and not fully expanded (such as Locale).
if (keys[0] && keys.length > 1 && flattenedOptions.indexOf(keys[0]) !== -1) {
var newKeys = [keys.shift()];
var property = '';
// Combine the remaining keys as a single property.
for (var ii = 0; ii < keys.length; ii++) {
property += (ii === 0 ? keys[ii] : keys[ii].charAt(0).toUpperCase() + keys[ii].slice(1));
}
newKeys.push(property);
keys = newKeys;
}
this.log(this.LOG_DEBUG, 'Processing data attribute "data-abs-' + name + '":', data[dataKeys[i]]);
expandObject(keys, data[dataKeys[i]], dataOptions);
}
this.options = $.extend(true, {}, this.options, dataOptions);
this.log(this.LOG_DEBUG, 'Merged in the data attribute options: ', dataOptions, this.options);
}
/**
* Reference to the selectpicker instance.
* @type {Selectpicker}
*/
this.selectpicker = data['selectpicker'];
if (!this.selectpicker) {
this.log(this.LOG_ERROR, 'Cannot instantiate an AjaxBootstrapSelect instance without selectpicker first being initialized!');
return null;
}
// Ensure there is a URL.
if (!this.options.ajax.url) {
this.log(this.LOG_ERROR, 'Option "ajax.url" must be set! Options:', this.options);
return null;
}
// Initialize the locale strings.
this.locale = $.extend(true, {}, $.fn.ajaxSelectPicker.locale);
// Ensure the langCode is properly set.
this.options.langCode = this.options.langCode || window.navigator.userLanguage || window.navigator.language || 'en';
if (!this.locale[this.options.langCode]) {
var langCode = this.options.langCode;
// Reset the language code.
this.options.langCode = 'en';
// Check for both the two and four character language codes, using
// the later first.
var langCodeArray = langCode.split('-');
for (i = 0, l = langCodeArray.length; i < l; i++) {
var code = langCodeArray.join('-');
if (code.length && this.locale[code]) {
this.options.langCode = code;
break;
}
langCodeArray.pop();
}
this.log(this.LOG_WARNING, 'Unknown langCode option: "' + langCode + '". Using the following langCode instead: "' + this.options.langCode + '".');
}
// Allow options to override locale specific strings.
this.locale[this.options.langCode] = $.extend(true, {}, this.locale[this.options.langCode], this.options.locale);
/**
* The select list.
* @type {AjaxBootstrapSelectList}
*/
this.list = new window.AjaxBootstrapSelectList(this);
this.list.refresh();
// We need for selectpicker to be attached first. Putting the init in a
// setTimeout is the easiest way to ensure this.
// @todo Figure out a better way to do this (hopefully listen for an event).
setTimeout(function () {
plugin.init();
}, 500);
};
/**
* Initializes this plugin on a selectpicker instance.
*/
AjaxBootstrapSelect.prototype.init = function () {
var requestDelayTimer, plugin = this;
// Rebind select/deselect to process preserved selections.
if (this.options.preserveSelected) {
this.selectpicker.$menu.off('click', '.actions-btn').on('click', '.actions-btn', function (e) {
if (plugin.selectpicker.options.liveSearch) {
plugin.selectpicker.$searchbox.focus();
}
else {
plugin.selectpicker.$button.focus();
}
e.preventDefault();
e.stopPropagation();
if ($(this).is('.bs-select-all')) {
if (plugin.selectpicker.$lis === null) {
plugin.selectpicker.$lis = plugin.selectpicker.$menu.find('li');
}
plugin.$element.find('option:enabled').prop('selected', true);
$(plugin.selectpicker.$lis).not('.disabled').addClass('selected');
plugin.selectpicker.render();
}
else {
if (plugin.selectpicker.$lis === null) {
plugin.selectpicker.$lis = plugin.selectpicker.$menu.find('li');
}
plugin.$element.find('option:enabled').prop('selected', false);
$(plugin.selectpicker.$lis).not('.disabled').removeClass('selected');
plugin.selectpicker.render();
}
plugin.selectpicker.$element.change();
});
}
// Add placeholder text to the search input.
this.selectpicker.$searchbox
.attr('placeholder', this.t('searchPlaceholder'))
// Remove selectpicker events on the search input.
.off('input propertychange');
// Bind this plugin's event.
this.selectpicker.$searchbox.on(this.options.bindEvent, function (e) {
var query = plugin.selectpicker.$searchbox.val();
plugin.log(plugin.LOG_DEBUG, 'Bind event fired: "' + plugin.options.bindEvent + '", keyCode:', e.keyCode, e);
// Dynamically ignore the "enter" key (13) so it doesn't
// create an additional request if the "cache" option has
// been disabled.
if (!plugin.options.cache) {
plugin.options.ignoredKeys[13] = 'enter';
}
// Don't process ignored keys.
if (plugin.options.ignoredKeys[e.keyCode]) {
plugin.log(plugin.LOG_DEBUG, 'Key ignored.');
return;
}
// Don't process if below minimum query length
if (query.length < plugin.options.minLength) {
plugin.list.setStatus(plugin.t('statusTooShort'));
return;
}
// Clear out any existing timer.
clearTimeout(requestDelayTimer);
// Process empty search value.
if (!query.length) {
// Clear the select list.
if (plugin.options.clearOnEmpty) {
plugin.list.destroy();
}
// Don't invoke a request.
if (!plugin.options.emptyRequest) {
return;
}
}
// Store the query.
plugin.previousQuery = plugin.query;
plugin.query = query;
// Return the cached results, if any.
if (plugin.options.cache && e.keyCode !== 13) {
var cache = plugin.list.cacheGet(plugin.query);
if (cache) {
plugin.list.setStatus(!cache.length ? plugin.t('statusNoResults') : '');
plugin.list.replaceOptions(cache);
plugin.log(plugin.LOG_INFO, 'Rebuilt options from cached data.');
return;
}
}
requestDelayTimer = setTimeout(function () {
// Abort any previous requests.
if (plugin.lastRequest && plugin.lastRequest.jqXHR && $.isFunction(plugin.lastRequest.jqXHR.abort)) {
plugin.lastRequest.jqXHR.abort();
}
// Create a new request.
plugin.request = new window.AjaxBootstrapSelectRequest(plugin);
// Store as the previous request once finished.
plugin.request.jqXHR.always(function () {
plugin.lastRequest = plugin.request;
plugin.request = false;
});
}, plugin.options.requestDelay || 300);
});
};
/**
* Wrapper function for logging messages to window.console.
*
* @param {Number} type
* The type of message to log. Must be one of:
*
* - AjaxBootstrapSelect.LOG_ERROR
* - AjaxBootstrapSelect.LOG_WARNING
* - AjaxBootstrapSelect.LOG_INFO
* - AjaxBootstrapSelect.LOG_DEBUG
*
* @param {String|Object|*...} message
* The message(s) to log. Multiple arguments can be passed.
*
* @return {void}
*/
AjaxBootstrapSelect.prototype.log = function (type, message) {
if (window.console && this.options.log) {
// Ensure the logging level is always an integer.
if (typeof this.options.log !== 'number') {
if (typeof this.options.log === 'string') {
this.options.log = this.options.log.toLowerCase();
}
switch (this.options.log) {
case true:
case 'debug':
this.options.log = this.LOG_DEBUG;
break;
case 'info':
this.options.log = this.LOG_INFO;
break;
case 'warn':
case 'warning':
this.options.log = this.LOG_WARNING;
break;
default:
case false:
case 'error':
this.options.log = this.LOG_ERROR;
break;
}
}
if (type <= this.options.log) {
var args = [].slice.apply(arguments, [2]);
// Determine the correct console method to use.
switch (type) {
case this.LOG_DEBUG:
type = 'debug';
break;
case this.LOG_INFO:
type = 'info';
break;
case this.LOG_WARNING:
type = 'warn';
break;
default:
case this.LOG_ERROR:
type = 'error';
break;
}
// Prefix the message.
var prefix = '[' + type.toUpperCase() + '] AjaxBootstrapSelect:';
if (typeof message === 'string') {
args.unshift(prefix + ' ' + message);
}
else {
args.unshift(message);
args.unshift(prefix);
}
// Display the message(s).
window.console[type].apply(window.console, args);
}
}
};
/**
* Replaces an old value in an object or array with a new value.
*
* @param {Object|Array} obj
* The object (or array) to iterate over.
* @param {*} needle
* The value to search for.
* @param {*} value
* The value to replace with.
* @param {Object} [options]
* Additional options for restricting replacement:
* - recursive: {boolean} Whether or not to iterate over the entire
* object or array, defaults to true.
* - depth: {int} The number of level this method is to search
* down into child elements, defaults to false (no limit).
* - limit: {int} The number of times a replacement should happen,
* defaults to false (no limit).
*
* @return {void}
*/
AjaxBootstrapSelect.prototype.replaceValue = function (obj, needle, value, options) {
var plugin = this;
options = $.extend({
recursive: true,
depth: false,
limit: false
}, options);
// The use of $.each() opposed to native loops here is beneficial
// since obj can be either an array or an object. This helps reduce
// the amount of duplicate code needed.
$.each(obj, function (k, v) {
if (options.limit !== false && typeof options.limit === 'number' && options.limit <= 0) {
return false;
}
if ($.isArray(obj[k]) || $.isPlainObject(obj[k])) {
if ((options.recursive && options.depth === false) || (options.recursive && typeof options.depth === 'number' && options.depth > 0)) {
plugin.replaceValue(obj[k], needle, value, options);
}
}
else {
if (v === needle) {
if (options.limit !== false && typeof options.limit === 'number') {
options.limit--;
}
obj[k] = value;
}
}
});
};
/**
* Generates a translated {@link $.fn.ajaxSelectPicker.locale locale string} for a given locale key.
*
* @param {String} key
* The translation key to use.
* @param {String} [langCode]
* Overrides the currently set {@link $.fn.ajaxSelectPicker.defaults#langCode langCode} option.
*
* @return
* The translated string.
*/
AjaxBootstrapSelect.prototype.t = function (key, langCode) {
langCode = langCode || this.options.langCode;
if (this.locale[langCode] && this.locale[langCode].hasOwnProperty(key)) {
return this.locale[langCode][key];
}
this.log(this.LOG_WARNING, 'Unknown translation key:', key);
return key;
};
/**
* Use an existing definition in the Window object or create a new one.
*
* Note: This must be the last statement of this file.
*
* @type {AjaxBootstrapSelect}
* @ignore
*/
window.AjaxBootstrapSelect = window.AjaxBootstrapSelect || AjaxBootstrapSelect;
/**
* @class AjaxBootstrapSelectList
* Maintains the select options and selectpicker menu.
*
* @param {AjaxBootstrapSelect} plugin
* The plugin instance.
*
* @return {AjaxBootstrapSelectList}
* A new instance of this class.
*/
var AjaxBootstrapSelectList = function (plugin) {
var that = this;
/**
* DOM element used for updating the status of requests and list counts.
* @type {jQuery}
*/
this.$status = $(plugin.options.templates.status).hide().appendTo(plugin.selectpicker.$menu);
var statusInitialized = plugin.t('statusInitialized');
if (statusInitialized && statusInitialized.length) {
this.setStatus(statusInitialized);
}
/**
* Container for cached data.
* @type {Object}
*/
this.cache = {};
/**
* Reference the plugin for internal use.
* @type {AjaxBootstrapSelect}
*/
this.plugin = plugin;
/**
* Container for current selections.
* @type {Array}
*/
this.selected = [];
/**
* Containers for previous titles.
*/
this.title = null;
this.selectedTextFormat = plugin.selectpicker.options.selectedTextFormat;
// Save initial options
var initial_options = [];
plugin.$element.find('option').each(function() {
var $option = $(this);
var value = $option.attr('value');
initial_options.push({
value: value,
text: $option.text(),
'class': $option.attr('class') || '',
data: $option.data() || {},
preserved: plugin.options.preserveSelected,
selected: !!$option.attr('selected')
});
});
this.cacheSet(/*query=*/'', initial_options);
// Preserve selected options.
if (plugin.options.preserveSelected) {
that.selected = initial_options;
plugin.$element.on('change.abs.preserveSelected', function (e) {
var $selected = plugin.$element.find(':selected');
that.selected = [];
// If select does not have multiple selection, ensure that only the
// last selected option is preserved.
if (!plugin.selectpicker.multiple) {
$selected = $selected.last();
}
$selected.each(function () {
var $option = $(this);
var value = $option.attr('value');
that.selected.push({
value: value,
text: $option.text(),
'class': $option.attr('class') || '',
data: $option.data() || {},
preserved: true,
selected: true
});
});
that.replaceOptions(that.cacheGet(that.plugin.query));
});
}
};
/**
* Builds the options for placing into the element.
*
* @param {Array} data
* The data to use when building options for the select list. Each
* array item must be an Object structured as follows:
* - {int|string} value: Required, a unique value identifying the
* item. Optionally not required if divider is passed instead.
* - {boolean} [divider]: Optional, if passed all other values are
* ignored and this item becomes a divider.
* - {string} [text]: Optional, the text to display for the item.
* If none is provided, the value will be used.
* - {String} [class]: Optional, the classes to apply to the option.
* - {boolean} [disabled]: Optional, flag that determines if the
* option is disabled.
* - {boolean} [selected]: Optional, flag that determines if the
* option is selected. Useful only for select lists that have the
* "multiple" attribute. If it is a single select list, each item
* that passes this property as true will void the previous one.
* - {Object} [data]: Optional, the additional data attributes to
* attach to the option element. These are processed by the
* bootstrap-select plugin.
*
* @return {String}
* HTML containing the <option> elements to place in the element.
*/
AjaxBootstrapSelectList.prototype.build = function (data) {
var a, i, l = data.length;
var $select = $('<select/>');
var $preserved = $('<optgroup/>').attr('label', this.plugin.t('currentlySelected'));
this.plugin.log(this.plugin.LOG_DEBUG, 'Building the select list options from data:', data);
for (i = 0; i < l; i++) {
var item = data[i];
var $option = $('<option/>').appendTo(item.preserved ? $preserved : $select);
// Detect dividers.
if (item.hasOwnProperty('divider')) {
$option.attr('data-divider', 'true');
continue;
}
// Set various properties.
$option.val(item.value).text(item.text).attr('title', item.text);
if (item['class'].length) {
$option.attr('class', item['class']);
}
if (item.disabled) {
$option.attr('disabled', true);
}
// Remove previous selections, if necessary.
if (item.selected && !this.plugin.selectpicker.multiple) {
$select.find(':selected').prop('selected', false);
}
// Set this option's selected state.
if (item.selected) {
$option.attr('selected', true);
}
// Add data attributes.
for (a in item.data) {
if (item.data.hasOwnProperty(a)) {
$option.attr('data-' + a, item.data[a]);
}
}
}
// Append the preserved selections.
if ($preserved.find('option').length) {
$preserved[this.plugin.options.preserveSelectedPosition === 'before' ? 'prependTo' : 'appendTo']($select);
}
var options = $select.html();
this.plugin.log(this.plugin.LOG_DEBUG, options);
return options;
};
/**
* Retrieve data from the cache.
*
* @param {string} key
* The identifier name of the data to retrieve.
* @param {*} [defaultValue]
* The default value to return if no cache data is available.
*
* @return {*}
* The cached data or defaultValue.
*/
AjaxBootstrapSelectList.prototype.cacheGet = function (key, defaultValue) {
var value = this.cache[key] || defaultValue;
this.plugin.log(this.LOG_DEBUG, 'Retrieving cache:', key, value);
return value;
};
/**
* Save data to the cache.
*
* @param {string} key
* The identifier name of the data to store.
* @param {*} value
* The value of the data to store.
*
* @return {void}
*/
AjaxBootstrapSelectList.prototype.cacheSet = function (key, value) {
this.cache[key] = value;
this.plugin.log(this.LOG_DEBUG, 'Saving to cache:', key, value);
};
/**
* Destroys the select list.
*/
AjaxBootstrapSelectList.prototype.destroy = function () {
this.replaceOptions();
this.plugin.list.setStatus();
this.plugin.log(this.plugin.LOG_DEBUG, 'Destroyed select list.');
};
/**
* Refreshes the select list.
*/
AjaxBootstrapSelectList.prototype.refresh = function (triggerChange) {
// Remove unnecessary "min-height" from selectpicker.
this.plugin.selectpicker.$menu.css('minHeight', 0);
this.plugin.selectpicker.$menu.find('> .inner').css('minHeight', 0);
var emptyTitle = this.plugin.t('emptyTitle');
if (!this.plugin.$element.find('option').length && emptyTitle && emptyTitle.length) {
this.setTitle(emptyTitle);
}
else if (
this.title ||
(
this.selectedTextFormat !== 'static' &&
this.selectedTextFormat !== this.plugin.selectpicker.options.selectedTextFormat
)
) {
this.restoreTitle();
}
this.plugin.selectpicker.refresh();
// The "refresh" method sets the $lis property to null, it must be rebuilt.
this.plugin.selectpicker.findLis();
// Only trigger change event when specified.
if(triggerChange){
this.plugin.log(this.plugin.LOG_DEBUG, 'Triggering Change');
this.plugin.$element.trigger('change.$');
}
this.plugin.log(this.plugin.LOG_DEBUG, 'Refreshed select list.');
};
/**
* Replaces the select list options with provided data.
*
* It will also inject any preserved selections if the preserveSelected
* option is enabled.
*
* @param {Array} data
* The data array to process.
*
* @returns {void}
*/
AjaxBootstrapSelectList.prototype.replaceOptions = function (data) {
var i, l, item, output = '', processedData = [], selected = [], seenValues = [];
data = data || [];
// Merge in selected options from the previous state (cannot be cached).
if (this.selected && this.selected.length) {
this.plugin.log(this.plugin.LOG_INFO, 'Processing preserved selections:', this.selected);
selected = [].concat(this.selected, data);
l = selected.length;
for (i = 0; i < l; i++) {
item = selected[i];
// Typecast the value for the seenValues array. Array indexOf
// searches are type sensitive.
if (item.hasOwnProperty('value') && seenValues.indexOf(item.value + '') === -1) {
seenValues.push(item.value + '');
processedData.push(item);
}
else {
this.plugin.log(this.plugin.LOG_DEBUG, 'Duplicate item found, ignoring.');
}
}
data = processedData;
}
// Build the option output.
if (data.length) {
output = this.plugin.list.build(data);
}
// Replace the options.
this.plugin.$element.html(output);
this.refresh();
this.plugin.log(this.plugin.LOG_DEBUG, 'Replaced options with data:', data);
};
/**
* Restores the select list to the last saved state.
*
* @return {boolean}
* Return true if successful or false if no states are present.
*/
AjaxBootstrapSelectList.prototype.restore = function () {
var cache = this.plugin.list.cacheGet(this.plugin.previousQuery);
if (cache && this.plugin.list.replaceOptions(cache)) {
this.plugin.log(this.plugin.LOG_DEBUG, 'Restored select list to the previous query: ', this.plugin.previousQuery);
}
this.plugin.log(this.plugin.LOG_DEBUG, 'Unable to restore select list to the previous query:', this.plugin.previousQuery);
return false;
};
/**
* Restores the previous title of the select element.
*
* @return {void}
*/
AjaxBootstrapSelectList.prototype.restoreTitle = function () {
if (!this.plugin.request) {
this.plugin.selectpicker.options.selectedTextFormat = this.selectedTextFormat;
if (this.title) {
this.plugin.$element.attr('title', this.title);
}
else {
this.plugin.$element.removeAttr('title');
}
this.title = null;
}
};
/**
* Sets a new title on the select element.
*
* @param {String} title
*
* @return {void}
*/
AjaxBootstrapSelectList.prototype.setTitle = function (title) {
if (!this.plugin.request) {
this.title = this.plugin.$element.attr('title');
this.plugin.selectpicker.options.selectedTextFormat = 'static';
this.plugin.$element.attr('title', title);
}
};
/**
* Sets a new status on the AjaxBootstrapSelectList.$status DOM element.
*
* @param {String} [status]
* The new status to set, if empty it will hide it.
*
* @return {void}
*/
AjaxBootstrapSelectList.prototype.setStatus = function (status) {
status = status || '';
if (status.length) {
this.$status.html(status).show();
}
else {
this.$status.html('').hide();
}
};
/**
* Use an existing definition in the Window object or create a new one.
*
* Note: This must be the last statement of this file.
*
* @type {AjaxBootstrapSelectList}
* @ignore
*/
window.AjaxBootstrapSelectList = window.AjaxBootstrapSelectList || AjaxBootstrapSelectList;
/**
* @class AjaxBootstrapSelectRequest
* Instantiates a new jQuery.ajax request for the current query.
*
* @param {AjaxBootstrapSelect} plugin
* The plugin instance.
*
* @return {AjaxBootstrapSelectRequest}
* A new instance of this class.
*/
var AjaxBootstrapSelectRequest = function (plugin) {
var that = this;
var ajaxCallback = function (event) {
return function () {
that.plugin.log(that.plugin.LOG_INFO, 'Invoking AjaxBootstrapSelectRequest.' + event + ' callback:', arguments);
that[event].apply(that, arguments);
if (that.callbacks[event]) {
that.plugin.log(that.plugin.LOG_INFO, 'Invoking ajax.' + event + ' callback:', arguments);
that.callbacks[event].apply(that, arguments);
}
};
};
var events = ['beforeSend', 'success', 'error', 'complete'];
var i, l = events.length;
// Reference the existing plugin.
this.plugin = plugin;
// Clone the default ajax options.
this.options = $.extend(true, {}, plugin.options.ajax);
// Save any existing callbacks provided in the options and replace it with
// the relevant method callback. The provided callback will be invoked
// after this plugin has executed.
this.callbacks = {};
for (i = 0; i < l; i++) {
var event = events[i];
if (this.options[event] && $.isFunction(this.options[event])) {
this.callbacks[event] = this.options[event];
}
this.options[event] = ajaxCallback(event);
}
// Allow the data option to be dynamically generated.
if (this.options.data && $.isFunction(this.options.data)) {
this.options.data = this.options.data.apply(this) || {
q: '{{{q}}}'
};
}
// Replace all data values that contain "{{{q}}}" with the value of the
// current search query.
this.plugin.replaceValue(this.options.data, '{{{q}}}', this.plugin.query);
// Invoke the AJAX request.
this.jqXHR = $.ajax(this.options);
};
/**
* @event
* A callback that can be used to modify the jqXHR object before it is sent.
*
* Use this to set custom headers, etc. Returning false will cancel the request.
* To modify the options being sent, use this.options.
*
* @param {jqXHR} jqXHR
* The jQuery wrapped XMLHttpRequest object.
*
* @return {void}
*/
AjaxBootstrapSelectRequest.prototype.beforeSend = function (jqXHR) {
// Destroy the list currently there.
this.plugin.list.destroy();
// Set the status accordingly.
this.plugin.list.setStatus(this.plugin.t('statusSearching'));
//this.plugin.list.refresh();
};
/**
* @event
* The "complete" callback for the request.
*
* @param {jqXHR} jqXHR
* The jQuery wrapped XMLHttpRequest object.
* @param {String} status
* A string categorizing the status of the request: "success", "notmodified",
* "error", "timeout", "abort", or "parsererror".
*
* @return {void}
*/
AjaxBootstrapSelectRequest.prototype.complete = function (jqXHR, status) {
// Only continue if actual results and not an aborted state.
if (status !== 'abort') {
var cache = this.plugin.list.cacheGet(this.plugin.query);
if (cache) {
if (cache.length) {
this.plugin.list.setStatus();
}
else {
this.plugin.list.destroy();
this.plugin.list.setStatus(this.plugin.t('statusNoResults'));
this.plugin.log(this.plugin.LOG_INFO, 'No results were returned.');
return;
}
}
this.plugin.list.refresh(true);
}
};
/**
* @event
* The "error" callback for the request.
*
* @param {jqXHR} jqXHR
* The jQuery wrapped XMLHttpRequest object.
* @param {String} status
* A string describing the type of error that occurred. Possible values for
* the second argument (besides null) are "timeout", "error", "abort", and
* "parsererror".
* @param {String|Object} error
* An optional exception object, if one occurred. When an HTTP error occurs,
* error receives the textual portion of the HTTP status, such as "Not Found"
* or "Internal Server Error."
*
* @return {void}
*/
AjaxBootstrapSelectRequest.prototype.error = function (jqXHR, status, error) {
if (status !== 'abort') {
// Cache the result data.
this.plugin.list.cacheSet(this.plugin.query);
// Clear the list.
if (this.plugin.options.clearOnError) {
this.plugin.list.destroy();
}
// Set the status after the list has cleared and before the restore.
this.plugin.list.setStatus(this.plugin.t('errorText'));
// Restore previous request.
if (this.plugin.options.restoreOnError) {
this.plugin.list.restore();
this.plugin.list.setStatus();
}
}
};
/**
* Process incoming data.
*
* This method ensures that the incoming data has unique values and
* is in the proper format that is utilized by this plugin. It also
* adds in the existing selects if the option is enabled. If the
* preprocessData and processData functions were defined in the plugin
* options, they are invoked here.
*
* @param {Array|Object} data
* The JSON data to process.
*
* @return {Array|Boolean}
* The processed data array or false if an error occurred.
*/
AjaxBootstrapSelectRequest.prototype.process = function (data) {
var i, l, callbackResult, item, preprocessedData, processedData;
var filteredData = [], seenValues = [];
this.plugin.log(this.plugin.LOG_INFO, 'Processing raw data for:', this.plugin.query, data);
// Invoke the preprocessData option callback.
preprocessedData = data;
if ($.isFunction(this.plugin.options.preprocessData)) {
this.plugin.log(this.plugin.LOG_DEBUG, 'Invoking preprocessData callback:', this.plugin.options.processData);
callbackResult = this.plugin.options.preprocessData.apply(this, [preprocessedData]);
if (typeof callbackResult !== 'undefined' && callbackResult !== null && callbackResult !== false) {
preprocessedData = callbackResult;
}
}
// Ensure the data is an array.
if (!$.isArray(preprocessedData)) {
this.plugin.log(this.plugin.LOG_ERROR, 'The data returned is not an Array. Use the "preprocessData" callback option to parse the results and construct a proper array for this plugin.', preprocessedData);
return false;
}
// Filter preprocessedData.
l = preprocessedData.length;
for (i = 0; i < l; i++) {
item = preprocessedData[i];
this.plugin.log(this.plugin.LOG_DEBUG, 'Processing item:', item);
if ($.isPlainObject(item)) {
// Check if item is a divider. If so, ignore all other data.
if (item.hasOwnProperty('divider') || (item.hasOwnProperty('data') && $.isPlainObject(item.data) && item.data.divider)) {
this.plugin.log(this.plugin.LOG_DEBUG, 'Item is a divider, ignoring provided data.');
filteredData.push({divider: true});
}
// Ensure item has a "value" and is unique.
else {
if (item.hasOwnProperty('value')) {
// Typecast the value for the seenValues array. Array
// indexOf searches are type sensitive.
if (seenValues.indexOf(item.value + '') === -1) {
seenValues.push(item.value + '');
// Provide default items to ensure expected structure.
item = $.extend({
text: item.value,
'class': '',
data: {},
disabled: false,
selected: false
}, item);
filteredData.push(item);
}
else {
this.plugin.log(this.plugin.LOG_DEBUG, 'Duplicate item found, ignoring.');
}
}
else {
this.plugin.log(this.plugin.LOG_DEBUG, 'Data item must have a "value" property, skipping.');
}
}
}
}
// Invoke the processData option callback.
processedData = [].concat(filteredData);
if ($.isFunction(this.plugin.options.processData)) {
this.plugin.log(this.plugin.LOG_DEBUG, 'Invoking processData callback:', this.plugin.options.processData);
callbackResult = this.plugin.options.processData.apply(this, [processedData]);
if (typeof callbackResult !== 'undefined' && callbackResult !== null && callbackResult !== false) {
if ($.isArray(callbackResult)) {
processedData = callbackResult;
}
else {
this.plugin.log(this.plugin.LOG_ERROR, 'The processData callback did not return an array.', callbackResult);
return false;
}
}
}
// Cache the processed data.
this.plugin.list.cacheSet(this.plugin.query, processedData);
this.plugin.log(this.plugin.LOG_INFO, 'Processed data:', processedData);
return processedData;
};
/**
* @event
* The "success" callback for the request.
*
* @param {Object} data
* The data returned from the server, formatted according to the dataType
* option.
* @param {String} status
* A string describing the status.
* @param {jqXHR} jqXHR
* The jQuery wrapped XMLHttpRequest object.
*
* @return {void}
*/
AjaxBootstrapSelectRequest.prototype.success = function (data, status, jqXHR) {
// Only process data if an Array or Object.
if (!$.isArray(data) && !$.isPlainObject(data)) {
this.plugin.log(this.plugin.LOG_ERROR, 'Request did not return a JSON Array or Object.', data);
this.plugin.list.destroy();
return;
}
// Process the data.
var processedData = this.process(data);
this.plugin.list.replaceOptions(processedData);
};
/**
* Use an existing definition in the Window object or create a new one.
*
* Note: This must be the last statement of this file.
*
* @type {AjaxBootstrapSelectRequest}
* @ignore
*/
window.AjaxBootstrapSelectRequest = window.AjaxBootstrapSelectRequest || AjaxBootstrapSelectRequest;
/**
* @class $.fn.ajaxSelectPicker
* @chainable
*
* The jQuery plugin definition.
*
* This initializes a new AjaxBootstrapSelect class for each element in the jQuery chain.
*
* @param {Object} options
* The {@link $.fn.ajaxSelectPicker.defaults options} to pass to the plugin.
*
* @returns {jQuery}
*/
$.fn.ajaxSelectPicker = function (options) {
return this.each(function () {
if (!$(this).data('AjaxBootstrapSelect')) {
$(this).data('AjaxBootstrapSelect', new window.AjaxBootstrapSelect(this, options));
}
});
};
/**
* The locale object containing string translations.
*
* See: {@link $.fn.ajaxSelectPicker.locale}
* @type {Object}
*/
$.fn.ajaxSelectPicker.locale = {};
/**
* The default options the plugin will use if none are provided.
*
* See: {@link $.fn.ajaxSelectPicker.defaults}
*
* @member $.fn.ajaxSelectPicker
* @property {Object} defaults
*/
$.fn.ajaxSelectPicker.defaults = {
/**
* @member $.fn.ajaxSelectPicker.defaults
* @deprecated Since version `1.2.0`, see: {@link $.fn.ajaxSelectPicker.defaults#preprocessData}.
* @cfg {Function} ajaxResultsPreHook
*/
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Object} ajax (required)
* @markdown
* The options to pass to the jQuery AJAX request.
*
* ```js
* {
* url: null, // Required.
* type: 'POST',
* dataType: 'json',
* data: {
* q: '{{{q}}}'
* }
* }
* ```
*/
ajax: {
url: null,
type: 'POST',
dataType: 'json',
data: {
q: '{{{q}}}'
}
},
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Number} minLength = 0
* @markdown
* Invoke a request for empty search values.
*/
minLength: 0,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {String} ajaxSearchUrl
* @deprecated Since version `1.2.0`, see: {@link $.fn.ajaxSelectPicker.defaults#ajax}.
*/
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {String} bindEvent = "keyup"
* @markdown
* The event to bind on the search input element to fire a request.
*/
bindEvent: 'keyup',
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} cache = true
* @markdown
* Cache previous requests. If enabled, the "enter" key (13) is enabled to
* allow users to force a refresh of the request.
*/
cache: true,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} clearOnEmpty = true
* @markdown
* Clears the previous results when the search input has no value.
*/
clearOnEmpty: true,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} clearOnError = true
* @markdown
* Clears the select list when the request returned with an error.
*/
clearOnError: true,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} debug
* @deprecated Since version `1.2.0`, see: {@link $.fn.ajaxSelectPicker.defaults#log}.
*/
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} emptyRequest = false
* @markdown
* Invoke a request for empty search values.
*/
emptyRequest: false,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Object} ignoredKeys
* @markdown
* Key codes to ignore so a request is not invoked with bindEvent. The
* "enter" key (13) will always be dynamically added to any list provided
* unless the "cache" option above is set to "true".
*
* ```js
* {
* 9: "tab",
* 16: "shift",
* 17: "ctrl",
* 18: "alt",
* 27: "esc",
* 37: "left",
* 39: "right",
* 38: "up",
* 40: "down",
* 91: "meta"
* }
* ```
*/
ignoredKeys: {
9: "tab",
16: "shift",
17: "ctrl",
18: "alt",
27: "esc",
37: "left",
39: "right",
38: "up",
40: "down",
91: "meta"
},
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {String} langCode = null
* @markdown
* The language code to use for string translation. By default this value
* is determined by the browser, however it is not entirely reliable. If
* you encounter inconsistencies, you may need to manually set this option.
*/
langCode: null,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Object} locale = null
* @markdown
* Provide specific overrides for {@link $.fn.ajaxSelectPicker.locale locale string} translations. Values
* set here will cause the plugin to completely ignore defined locale string
* translations provided by the set language code. This is useful when
* needing to change a single value or when being used in a system that
* provides its own translations, like a CMS.
*
* ```js
* locale: {
* searchPlaceholder: Drupal.t('Find...')
* }
* ```
*/
locale: null,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {String|Number|Number} log = 'error'
* @markdown
* Determines the amount of logging that is displayed:
*
* - __0, false:__ Display no information from the plugin.
* - __1, 'error':__ Fatal errors that prevent the plugin from working.
* - __2, 'warn':__ Warnings that may impact the display of request data, but does not prevent the plugin from functioning.
* - __3, 'info':__ Provides additional information, generally regarding the from request data and callbacks.
* - __4, true, 'debug':__ Display all possible information. This will likely be highly verbose and is only recommended for development purposes or tracing an error with a request.
*/
log: 'error',
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} mixWithCurrents
* @deprecated Since version `1.2.0`, see: {@link $.fn.ajaxSelectPicker.defaults#preserveSelected}.
*/
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg placeHolderOption
* @deprecated Since version `1.2.0`, see: {@link $.fn.ajaxSelectPicker.locale#emptyTitle}.
*/
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Function|null} preprocessData = function(){}
* @markdown
* Process the raw data returned from a request.
*
* The following arguments are passed to this callback:
*
* - __data__ - `Array` The raw data returned from the request, passed by reference.
*
* This callback must return one of the following:
*
* - `Array` - A new array of items. This will replace the passed data.
* - `undefined|null|false` - Stops the callback and will use whatever modifications have been made to data.
*
* ```js
* function (data) {
* var new = [], old = [], other = [];
* for (var i = 0; i < data.length; i++) {
* // Add items flagged as "new" to the correct array.
* if (data[i].new) {
* new.push(data[i]);
* }
* // Add items flagged as "old" to the correct array.
* else if (data[i].old) {
* old.push(data[i]);
* }
* // Something out of the ordinary happened, put these last.
* else {
* other.push(data[i]);
* }
* }
* // Sort the data according to the order of these arrays.
* return [].concat(new, old, other).
* }
* ```
*/
preprocessData: function () { },
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} preserveSelected = true
* @markdown
* Preserve selected items(s) between requests. When enabled, they will be
* placed in an `<optgroup>` with the label `Currently Selected`. Disable
* this option if you send your currently selected items along with your
* request and let the server handle this responsibility.
*/
preserveSelected: true,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {String} preserveSelectedPosition = 'after'
* @markdown
* Place the currently selected options `'before'` or `'after'` the options
* returned from the request.
*/
preserveSelectedPosition: 'after',
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Function|null} processData = function(){}
* @markdown
* Process the data returned after this plugin, but before the list is built.
*/
processData: function () { },
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Number} requestDelay = 300
* @markdown
* The amount of time, in milliseconds, that must pass before a request
* is initiated. Each time the {@link $.fn.ajaxSelectPicker.defaults#bindEvent bindEvent} is fired, it will cancel the
* current delayed request and start a new one.
*/
requestDelay: 300,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Boolean} restoreOnError = false
* @markdown
* Restores the select list with the previous results when the request
* returns with an error.
*/
restoreOnError: false,
/**
* @member $.fn.ajaxSelectPicker.defaults
* @cfg {Object} templates
* @markdown
* The DOM templates used in this plugin.
*
* ```js
* templates: {
* // The placeholder for status updates pertaining to the list and request.
* status: '<div class="status"></div>',
* }
* ```
*/
templates: {
status: '<div class="status"></div>'
}
};
/*
* Note: You do not have to load this translation file. English is the
* default language of this plugin and is compiled into it automatically.
*
* This file is just to serve as the default string mappings and as a
* template for future translations.
* @see: ./src/js/locale/en-US.js
*
* Four character language codes are supported ("en-US") and will always
* take precedence over two character language codes ("en") if present.
*
* When copying this file, remove all comments except the one above the
* definition objection giving credit to the translation author.
*/
/*!
* English translation for the "en-US" and "en" language codes.
* Mark Carver <mark.carver@me.com>
*/
$.fn.ajaxSelectPicker.locale['en-US'] = {
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} currentlySelected = 'Currently Selected'
* @markdown
* The text to use for the label of the option group when currently selected options are preserved.
*/
currentlySelected: 'Currently Selected',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} emptyTitle = 'Select and begin typing'
* @markdown
* The text to use as the title for the select element when there are no items to display.
*/
emptyTitle: 'Select and begin typing',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} errorText = ''Unable to retrieve results'
* @markdown
* The text to use in the status container when a request returns with an error.
*/
errorText: 'Unable to retrieve results',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} searchPlaceholder = 'Search...'
* @markdown
* The text to use for the search input placeholder attribute.
*/
searchPlaceholder: 'Search...',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} statusInitialized = 'Start typing a search query'
* @markdown
* The text used in the status container when it is initialized.
*/
statusInitialized: 'Start typing a search query',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} statusNoResults = 'No Results'
* @markdown
* The text used in the status container when the request returns no results.
*/
statusNoResults: 'No Results',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} statusSearching = 'Searching...'
* @markdown
* The text to use in the status container when a request is being initiated.
*/
statusSearching: 'Searching...',
/**
* @member $.fn.ajaxSelectPicker.locale
* @cfg {String} statusToShort = 'Please enter more characters'
* @markdown
* The text used in the status container when the request returns no results.
*/
statusTooShort: 'Please enter more characters'
};
$.fn.ajaxSelectPicker.locale.en = $.fn.ajaxSelectPicker.locale['en-US'];
})(jQuery, window);