You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1290 lines
34 KiB
1290 lines
34 KiB
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
|
|
(function($) {
|
|
var ZBX_STYLE_CLASS = 'multiselect-control';
|
|
const MS_ACTION_POPUP = 0;
|
|
const MS_ACTION_AUTOSUGGEST = 1;
|
|
|
|
/**
|
|
* Multi select helper.
|
|
*
|
|
* @param string options['object_name'] backend data source
|
|
* @param object options['objectOptions'] parameters to be added the request URL (optional)
|
|
*
|
|
* @see jQuery.multiSelect()
|
|
*/
|
|
$.fn.multiSelectHelper = function(options) {
|
|
options = $.extend({objectOptions: {}}, options);
|
|
|
|
var curl = new Curl('jsrpc.php');
|
|
curl.setArgument('type', 11); // PAGE_TYPE_TEXT_RETURN_JSON
|
|
curl.setArgument('method', 'multiselect.get');
|
|
curl.setArgument('object_name', options.object_name);
|
|
|
|
for (var key in options.objectOptions) {
|
|
curl.setArgument(key, options.objectOptions[key]);
|
|
}
|
|
|
|
options.url = curl.getUrl();
|
|
|
|
return this.each(function() {
|
|
$(this).empty().multiSelect(options);
|
|
});
|
|
};
|
|
|
|
/*
|
|
* Multiselect methods
|
|
*/
|
|
var methods = {
|
|
/**
|
|
* Get multi select selected data.
|
|
*
|
|
* @return array array of multiselect value objects
|
|
*/
|
|
getData: function() {
|
|
var $obj = $(this).first(),
|
|
ms = $obj.data('multiSelect');
|
|
|
|
var data = [];
|
|
for (var id in ms.values.selected) {
|
|
var item = ms.values.selected[id];
|
|
|
|
data.push({
|
|
id: id,
|
|
name: item.name,
|
|
prefix: (typeof item.prefix === 'undefined') ? '' : item.prefix
|
|
});
|
|
}
|
|
|
|
// Sort entries by name field.
|
|
data.sort(function(a, b) {
|
|
if (a.name === b.name) {
|
|
return 0;
|
|
}
|
|
else {
|
|
return (a.name < b.name) ? -1 : 1;
|
|
}
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* Insert outside data
|
|
*
|
|
* @param {array} items Multiselect value object.
|
|
* @param {bool} trigger_change (optional) Either to trigger element on-change event once data added. True by default.
|
|
*
|
|
* @return jQuery
|
|
*/
|
|
addData: function(items, trigger_change) {
|
|
return this.each(function() {
|
|
var $obj = $(this),
|
|
ms = $obj.data('multiSelect');
|
|
|
|
if (typeof trigger_change !== 'boolean') {
|
|
trigger_change = true;
|
|
}
|
|
|
|
if (ms.options.selectedLimit == 1) {
|
|
for (var id in ms.values.selected) {
|
|
removeSelected($obj, id);
|
|
}
|
|
}
|
|
|
|
for (var i = 0, l = items.length; i < l; i++) {
|
|
addSelected($obj, items[i]);
|
|
}
|
|
|
|
trigger_change && $obj.trigger('change', ms);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Enable multi select UI control.
|
|
*
|
|
* @return jQuery
|
|
*/
|
|
enable: function() {
|
|
return this.each(function() {
|
|
var $obj = $(this),
|
|
ms = $obj.data('multiSelect');
|
|
|
|
if (ms.options.disabled === true) {
|
|
$obj.removeAttr('aria-disabled');
|
|
$('.multiselect-list', $obj).removeClass('disabled');
|
|
$('.multiselect-button > button', $obj.parent()).prop('disabled', false);
|
|
$obj.append(makeMultiSelectInput($obj));
|
|
|
|
ms.options.disabled = false;
|
|
|
|
cleanSearch($obj);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Disable multi select UI control.
|
|
*
|
|
* @return jQuery
|
|
*/
|
|
disable: function() {
|
|
return this.each(function() {
|
|
var $obj = $(this),
|
|
ms = $obj.data('multiSelect');
|
|
|
|
if (ms.options.disabled === false) {
|
|
$obj.attr('aria-disabled', true);
|
|
$('.multiselect-list', $obj).addClass('disabled');
|
|
$('.multiselect-button > button', $obj.parent()).prop('disabled', true);
|
|
$('input[type="text"]', $obj).remove();
|
|
|
|
ms.options.disabled = true;
|
|
|
|
cleanSearch($obj);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Clean multi select object values.
|
|
*
|
|
* @return jQuery
|
|
*/
|
|
clean: function() {
|
|
return this.each(function() {
|
|
var $obj = $(this),
|
|
ms = $obj.data('multiSelect');
|
|
|
|
for (var id in ms.values.selected) {
|
|
removeSelected($obj, id);
|
|
}
|
|
|
|
cleanSearch($obj);
|
|
|
|
$obj.trigger('change', ms);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Modify one or more multiselect options after multiselect object has been created.
|
|
*
|
|
* @return jQuery
|
|
*/
|
|
modify: function(options) {
|
|
return this.each(function() {
|
|
var $obj = $(this),
|
|
ms = $obj.data('multiSelect');
|
|
|
|
var addNew_modified = ('addNew' in options) && options['addNew'] != ms.options['addNew'];
|
|
|
|
for (var key in ms.options) {
|
|
if (key in options) {
|
|
ms.options[key] = options[key];
|
|
}
|
|
}
|
|
|
|
if (addNew_modified) {
|
|
/*
|
|
* When modifying the "addNew" option, few things must be done:
|
|
* 1. Search input must be reset.
|
|
* 2. The already selected "(new)" items must be either hidden and disabled or shown and enabled.
|
|
* Note: hidden and disabled items will not submit to the server.
|
|
* 3. The "change" trigger must fire.
|
|
*/
|
|
|
|
cleanSearch($obj);
|
|
|
|
$('input[name*="[new]"]', $obj)
|
|
.prop('disabled', !ms.options['addNew'])
|
|
.each(function() {
|
|
var id = $(this).val();
|
|
$('.selected li[data-id]', $obj).each(function() {
|
|
if ($(this).data('id') == id) {
|
|
$(this).toggle(ms.options['addNew']);
|
|
}
|
|
});
|
|
});
|
|
|
|
$obj.trigger('change', ms);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Return option value.
|
|
*
|
|
* @return string
|
|
*/
|
|
getOption: function(key) {
|
|
var ret = null;
|
|
this.each(function() {
|
|
var $obj = $(this);
|
|
|
|
if ($obj.data('multiSelect') !== undefined) {
|
|
ret = $obj.data('multiSelect').options[key];
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
},
|
|
|
|
/**
|
|
* @return HTMLElement
|
|
*/
|
|
getSelectButton: function() {
|
|
var ret = null;
|
|
|
|
this.each(function() {
|
|
var $obj = $(this);
|
|
|
|
if ($obj.data('multiSelect') !== undefined) {
|
|
ret = $obj.data('multiSelect').select_button[0];
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
},
|
|
|
|
/**
|
|
* @param array entries IDs to mark disabled.
|
|
*/
|
|
setDisabledEntries: function (entries) {
|
|
this.each(function() {
|
|
const $obj = $(this);
|
|
const ms_parameters = $obj.data('multiSelect');
|
|
|
|
if (typeof ms_parameters === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const link = new Curl(ms_parameters.options.url);
|
|
link.setArgument('disabledids', entries);
|
|
|
|
ms_parameters.options.url = link.getUrl();
|
|
ms_parameters.options.popup.parameters.disableids = entries;
|
|
|
|
$obj.data('multiSelect', ms_parameters);
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create multi select input element.
|
|
*
|
|
* @param string options['url'] backend url
|
|
* @param string options['name'] input element name
|
|
* @param object options['labels'] translated labels (optional)
|
|
* @param object options['data'] preload data {id, name, prefix} (optional)
|
|
* @param string options['data'][id]
|
|
* @param string options['data'][name]
|
|
* @param string options['data'][prefix] (optional)
|
|
* @param bool options['data'][inaccessible] (optional)
|
|
* @param bool options['data'][disabled] (optional)
|
|
* @param string options['placeholder'] set custom placeholder (optional)
|
|
* @param array options['excludeids'] the list of excluded ids (optional)
|
|
* @param string options['defaultValue'] default value for input element (optional)
|
|
* @param bool options['disabled'] turn on/off readonly state (optional)
|
|
* @param bool options['hidden'] hide element (optional)
|
|
* @param bool options['addNew'] allow user to create new names (optional)
|
|
* @param int options['selectedLimit'] how many items can be selected (optional)
|
|
* @param int options['limit'] how many available items can be received from backend (optional)
|
|
* @param object options['popup'] popup data {parameters, width, height} (optional)
|
|
* @param string options['popup']['parameters']
|
|
* @param string options['popup']['filter_preselect']
|
|
* @param string options['popup']['filter_preselect']['id']
|
|
* @param string options['popup']['filter_preselect']['submit_as']
|
|
* @param object options['popup']['filter_preselect']['submit_parameters']
|
|
* @param bool options['popup']['filter_preselect']['multiple']
|
|
* @param int options['popup']['width']
|
|
* @param int options['popup']['height']
|
|
* @param object options['autosuggest'] autosuggest options (optional)
|
|
* @param object options['autosuggest']['filter_preselect']
|
|
* @param string options['autosuggest']['filter_preselect']['id']
|
|
* @param string options['autosuggest']['filter_preselect']['submit_as']
|
|
* @param object options['autosuggest']['filter_preselect']['submit_parameters']
|
|
* @param bool options['autosuggest']['filter_preselect']['multiple']
|
|
* @param string options['styles'] additional style for multiselect wrapper HTML element (optional)
|
|
* @param string options['styles']['property']
|
|
* @param string options['styles']['value']
|
|
*
|
|
* @return object
|
|
*/
|
|
$.fn.multiSelect = function(options) {
|
|
// Call a public method.
|
|
if (methods[options]) {
|
|
return methods[options].apply(this, Array.prototype.slice.call(arguments, 1));
|
|
}
|
|
|
|
var defaults = {
|
|
url: '',
|
|
name: '',
|
|
labels: {
|
|
'No matches found': t('No matches found'),
|
|
'More matches found...': t('More matches found...'),
|
|
'type here to search': t('type here to search'),
|
|
'new': t('new'),
|
|
'Select': t('Select')
|
|
},
|
|
placeholder: t('type here to search'),
|
|
data: [],
|
|
excludeids: [],
|
|
addNew: false,
|
|
defaultValue: null,
|
|
disabled: false,
|
|
selectedLimit: 0,
|
|
limit: 20,
|
|
popup: {},
|
|
styles: {}
|
|
};
|
|
|
|
options = $.extend({}, defaults, options);
|
|
|
|
return this.each(function() {
|
|
var $obj = $(this);
|
|
|
|
if ($obj.data('multiSelect') !== undefined) {
|
|
return;
|
|
}
|
|
|
|
options.required_str = $obj.attr('aria-required') === undefined ? 'false' : $obj.attr('aria-required');
|
|
$obj.removeAttr('aria-required');
|
|
|
|
var ms = {
|
|
options: options,
|
|
values: {
|
|
search: '',
|
|
searches: {},
|
|
searching: {},
|
|
selected: {},
|
|
available: {},
|
|
available_div: $('<div>', {'class': 'multiselect-available'}),
|
|
|
|
/*
|
|
* Indicates a false click on an available list, but not on some actual item.
|
|
* In such case the "focusout" event (IE) of the search input should not be processed.
|
|
*/
|
|
available_false_click: false
|
|
},
|
|
select_button: null
|
|
};
|
|
|
|
ms.values.available_div.on('mousedown', 'li', function() {
|
|
/*
|
|
* Cancel event propagation if actual available item was clicked.
|
|
* As a result, the "focusout" event of the search input will not fire in all browsers.
|
|
*/
|
|
return false;
|
|
});
|
|
|
|
$obj.data('multiSelect', ms);
|
|
|
|
$obj.wrap($('<div>', {
|
|
'class': ZBX_STYLE_CLASS,
|
|
css: ms.options.styles
|
|
}));
|
|
|
|
var $selected_div = $('<div>', {'class': 'selected'}).on('click', function() {
|
|
/*
|
|
* Focus without options because here it don't matter.
|
|
* Click used instead focus because in patternselect listen only click.
|
|
*/
|
|
$('input[type="text"]', $obj).click().focus();
|
|
}),
|
|
$selected_ul = $('<ul>', {'class': 'multiselect-list'});
|
|
|
|
$obj.append($selected_div.append($selected_ul));
|
|
|
|
if (ms.options.disabled) {
|
|
$obj.attr('aria-disabled', true);
|
|
$selected_ul.addClass('disabled');
|
|
}
|
|
else {
|
|
$obj.append(makeMultiSelectInput($obj));
|
|
}
|
|
|
|
$obj
|
|
.on('mousedown', function(event) {
|
|
if (isSearchFieldVisible($obj) && ms.options.selectedLimit != 1) {
|
|
$obj.addClass('active');
|
|
$('.selected li.selected', $obj).removeClass('selected');
|
|
|
|
// Focus input only when not clicked on selected item.
|
|
if (!$(event.target).parents('.multiselect-list').length) {
|
|
$('input[type="text"]', $obj).focus();
|
|
}
|
|
}
|
|
})
|
|
.on('remove', function() {
|
|
cleanSearch($obj);
|
|
});
|
|
|
|
if (empty(ms.options.data)) {
|
|
addDefaultValue($obj);
|
|
}
|
|
else {
|
|
$.each(ms.options.data, function(i, item) {
|
|
addSelected($obj, item);
|
|
});
|
|
}
|
|
|
|
if (ms.options.custom_select || ms.options.popup.parameters !== undefined) {
|
|
ms.select_button = $('<button>', {
|
|
type: 'button',
|
|
'class': 'btn-grey',
|
|
text: ms.options.labels['Select']
|
|
});
|
|
|
|
if (ms.options.disabled) {
|
|
ms.select_button.prop('disabled', true);
|
|
}
|
|
|
|
if (ms.options.popup.parameters !== undefined) {
|
|
ms.select_button.on('click', function(event) {
|
|
var parameters = ms.options.popup.parameters;
|
|
|
|
if (ms.options.popup.filter_preselect) {
|
|
parameters = jQuery.extend(parameters, getFilterPreselect($obj, MS_ACTION_POPUP));
|
|
}
|
|
|
|
if (typeof parameters['disable_selected'] !== 'undefined' && parameters['disable_selected']) {
|
|
parameters['disableids'] = Object.keys(ms.values.selected);
|
|
}
|
|
|
|
// Click used instead focus because in patternselect only click is listened for.
|
|
$('input[type="text"]', $obj).click();
|
|
|
|
return PopUp('popup.generic', parameters, {
|
|
dialogue_class: 'modal-popup-generic',
|
|
trigger_element: event.target
|
|
});
|
|
});
|
|
}
|
|
|
|
$obj.after($('<div>', {'class': 'multiselect-button'}).append(ms.select_button));
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get current value from preselect filter field.
|
|
*
|
|
* @param {jQuery} $obj Multiselect instance.
|
|
* @param {int} action User action that caused preselection.
|
|
*
|
|
* @return {object}
|
|
*/
|
|
function getFilterPreselect($obj, action) {
|
|
const ms = $obj.data('multiSelect');
|
|
const options_key = action == MS_ACTION_AUTOSUGGEST ? 'autosuggest' : 'popup';
|
|
|
|
if (!(options_key in ms.options) || !('filter_preselect' in ms.options[options_key])) {
|
|
return {};
|
|
}
|
|
|
|
const filter_preselect = ms.options[options_key].filter_preselect;
|
|
const data = $('#' + filter_preselect.id).multiSelect('getData');
|
|
|
|
if (data.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
let ret = {};
|
|
|
|
if ('multiple' in filter_preselect && filter_preselect.multiple) {
|
|
ret[filter_preselect.submit_as] = [];
|
|
|
|
for (const item of data) {
|
|
ret[filter_preselect.submit_as].push(item.id);
|
|
}
|
|
}
|
|
else {
|
|
ret[filter_preselect.submit_as] = data[0].id;
|
|
}
|
|
|
|
if ('submit_parameters' in filter_preselect) {
|
|
ret = {...ret, ...filter_preselect.submit_parameters};
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
function makeMultiSelectInput($obj) {
|
|
var ms = $obj.data('multiSelect'),
|
|
$label = $('label[for=' + $obj.attr('id') + '_ms]'),
|
|
$aria_live = $('[aria-live]', $obj),
|
|
$input = $('<input>', {
|
|
'id': $label.length ? $label.attr('for') : null,
|
|
'class': 'input',
|
|
'type': 'text',
|
|
'autocomplete': 'off',
|
|
'placeholder': ms.options.placeholder,
|
|
'aria-label': ($label.length ? $label.text() + '. ' : '') + ms.options.placeholder,
|
|
'aria-required': ms.options.required_str
|
|
})
|
|
.on('keyup', function(e) {
|
|
switch (e.which) {
|
|
case KEY_ARROW_DOWN:
|
|
case KEY_ARROW_LEFT:
|
|
case KEY_ARROW_RIGHT:
|
|
case KEY_ARROW_UP:
|
|
case KEY_ESCAPE:
|
|
return false;
|
|
}
|
|
|
|
clearSearchTimeout($obj);
|
|
|
|
// Maximum results selected already?
|
|
if (ms.options.selectedLimit != 0 && $('.selected li', $obj).length >= ms.options.selectedLimit) {
|
|
return false;
|
|
}
|
|
|
|
var search = $input.val();
|
|
|
|
if (search !== '') {
|
|
search = search.trim();
|
|
|
|
$('.selected li.selected', $obj).removeClass('selected');
|
|
}
|
|
|
|
if (search !== '') {
|
|
var preselect_values = getFilterPreselect($obj, MS_ACTION_AUTOSUGGEST),
|
|
cache_key = search + JSON.stringify(preselect_values);
|
|
|
|
/*
|
|
* Strategy:
|
|
* 1. Load the cached result set if such exists for the given term and show the list.
|
|
* 2. Skip anything if already expecting the result set to arrive for the given term.
|
|
* 3. Schedule result set retrieval for the given term otherwise.
|
|
*/
|
|
|
|
if (cache_key in ms.values.searches) {
|
|
ms.values.search = search;
|
|
ms.values.cache_key = cache_key;
|
|
loadAvailable($obj);
|
|
showAvailable($obj);
|
|
}
|
|
else if (!(cache_key in ms.values.searching)) {
|
|
ms.values.searchTimeout = setTimeout(function() {
|
|
ms.values.searching[cache_key] = true;
|
|
|
|
$.ajax({
|
|
url: ms.options.url + '&curtime=' + new CDate().getTime(),
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
cache: false,
|
|
data: jQuery.extend({
|
|
search: search,
|
|
limit: getLimit($obj),
|
|
}, preselect_values)
|
|
})
|
|
.then(function(response) {
|
|
ms.values.searches[cache_key] = response.result;
|
|
|
|
if (search === $input.val().trim()) {
|
|
ms.values.search = search;
|
|
ms.values.cache_key = cache_key;
|
|
loadAvailable($obj);
|
|
showAvailable($obj);
|
|
}
|
|
})
|
|
.done(function() {
|
|
delete ms.values.searching[cache_key];
|
|
});
|
|
|
|
delete ms.values.searchTimeout;
|
|
}, 500);
|
|
}
|
|
}
|
|
else {
|
|
hideAvailable($obj);
|
|
}
|
|
})
|
|
.on('keydown', function(e) {
|
|
switch (e.which) {
|
|
case KEY_TAB:
|
|
case KEY_ESCAPE:
|
|
var $available = ms.values.available_div;
|
|
|
|
if ($available.parent().is(document.body)) {
|
|
hideAvailable($obj);
|
|
e.stopPropagation();
|
|
}
|
|
|
|
cleanSearchInput($obj);
|
|
break;
|
|
|
|
case KEY_ENTER:
|
|
if ($input.val() !== '') {
|
|
var $selected = $('li.suggest-hover', ms.values.available_div);
|
|
|
|
if ($selected.length) {
|
|
select($obj, $selected.data('id'));
|
|
$aria_live.text(sprintf(t('Added, %1$s'), $selected.data('label')));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case KEY_ARROW_LEFT:
|
|
if ($input.val() === '') {
|
|
var $collection = $('.selected li', $obj);
|
|
|
|
if ($collection.length) {
|
|
var $prev = $collection.filter('.selected').removeClass('selected').prev();
|
|
$prev = ($prev.length ? $prev : $collection.last()).addClass('selected');
|
|
|
|
$aria_live.text(($prev.hasClass('disabled'))
|
|
? sprintf(t('%1$s, read only'), $prev.data('label'))
|
|
: $prev.data('label')
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_ARROW_RIGHT:
|
|
if ($input.val() === '') {
|
|
var $collection = $('.selected li', $obj);
|
|
|
|
if ($collection.length) {
|
|
var $next = $collection.filter('.selected').removeClass('selected').next();
|
|
$next = ($next.length ? $next : $collection.first()).addClass('selected');
|
|
|
|
$aria_live.text(($next.hasClass('disabled'))
|
|
? sprintf(t('%1$s, read only'), $next.data('label'))
|
|
: $next.data('label')
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_ARROW_UP:
|
|
case KEY_ARROW_DOWN:
|
|
var $collection = $('li', ms.values.available_div.filter(':visible')),
|
|
$selected = $collection.filter('.suggest-hover').removeClass('suggest-hover');
|
|
|
|
if ($selected.length) {
|
|
$selected = (e.which == KEY_ARROW_UP)
|
|
? ($selected.is(':first-child') ? $collection.last() : $selected.prev())
|
|
: ($selected.is(':last-child') ? $collection.first() : $selected.next());
|
|
|
|
$selected.addClass('suggest-hover');
|
|
$aria_live.text($selected.data('label'));
|
|
}
|
|
|
|
scrollAvailable($obj);
|
|
|
|
return false;
|
|
|
|
case KEY_BACKSPACE:
|
|
case KEY_DELETE:
|
|
if ($input.val() === '') {
|
|
var $selected = $('.selected li.selected', $obj);
|
|
|
|
if ($selected.length) {
|
|
var id = $selected.data('id'),
|
|
item = ms.values.selected[id];
|
|
|
|
if (typeof item.disabled === 'undefined' || !item.disabled) {
|
|
var aria_text = sprintf(t('Removed, %1$s'), $selected.data('label'));
|
|
|
|
$selected = (e.which == KEY_BACKSPACE)
|
|
? ($selected.is(':first-child') ? $selected.next() : $selected.prev())
|
|
: ($selected.is(':last-child') ? $selected.prev() : $selected.next());
|
|
|
|
removeSelected($obj, id);
|
|
|
|
$obj.trigger('change', ms);
|
|
|
|
if ($selected.length) {
|
|
var $collection = $('.selected li', $obj);
|
|
$selected.addClass('selected');
|
|
|
|
aria_text += ', ' + sprintf(
|
|
($selected.hasClass('disabled'))
|
|
? t('Selected, %1$s, read only, in position %2$d of %3$d')
|
|
: t('Selected, %1$s in position %2$d of %3$d'),
|
|
$selected.data('label'),
|
|
$collection.index($selected) + 1,
|
|
$collection.length
|
|
);
|
|
}
|
|
|
|
$aria_live.text(aria_text);
|
|
}
|
|
else {
|
|
$aria_live.text(t('Cannot be removed'));
|
|
}
|
|
}
|
|
else if (e.which == KEY_BACKSPACE) {
|
|
/*
|
|
* Pressing Backspace on empty input field should select last element in
|
|
* multiselect. For next Backspace press to be able to remove it.
|
|
*/
|
|
var $selected = $('.selected li:last-child', $obj).addClass('selected');
|
|
$aria_live.text($selected.data('label'));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
})
|
|
.on('focusin', function($event) {
|
|
$obj.addClass('active');
|
|
})
|
|
.on('focusout', function($event) {
|
|
if (ms.values.available_false_click) {
|
|
ms.values.available_false_click = false;
|
|
$('input[type="text"]', $obj)[0].focus({preventScroll:true});
|
|
}
|
|
else {
|
|
$obj.removeClass('active');
|
|
$('.selected li.selected', $obj).removeClass('selected');
|
|
cleanSearchInput($obj);
|
|
hideAvailable($obj);
|
|
}
|
|
});
|
|
|
|
return $input;
|
|
}
|
|
|
|
function addDefaultValue($obj) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
if (!empty(ms.options.defaultValue)) {
|
|
$obj.append($('<input>', {
|
|
type: 'hidden',
|
|
name: ms.options.name,
|
|
value: ms.options.defaultValue,
|
|
'data-default': 1
|
|
}));
|
|
}
|
|
}
|
|
|
|
function removeDefaultValue($obj) {
|
|
$('input[data-default="1"]', $obj).remove();
|
|
}
|
|
|
|
function select($obj, id) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
addSelected($obj, ms.values.available[id]);
|
|
|
|
if (isSearchFieldVisible($obj)) {
|
|
$('input[type="text"]', $obj)[0].focus({preventScroll:true});
|
|
}
|
|
|
|
$obj.trigger('change', ms);
|
|
}
|
|
|
|
function addSelected($obj, item) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
if (item.id in ms.values.selected) {
|
|
return;
|
|
}
|
|
|
|
removeDefaultValue($obj);
|
|
ms.values.selected[item.id] = item;
|
|
|
|
var prefix = (item.prefix || ''),
|
|
item_disabled = (typeof item.disabled !== 'undefined' && item.disabled);
|
|
|
|
$obj.append($('<input>', {
|
|
type: 'hidden',
|
|
name: (ms.options.addNew && item.isNew) ? ms.options.name + '[new]' : ms.options.name,
|
|
value: item.id,
|
|
'data-name': item.name,
|
|
'data-prefix': prefix
|
|
}));
|
|
|
|
var $li = $('<li>', {
|
|
'data-id': item.id,
|
|
'data-label': prefix + item.name
|
|
})
|
|
.append(
|
|
$('<span>', {
|
|
'class': 'subfilter-enabled'
|
|
})
|
|
.append($('<span>', {
|
|
text: prefix + item.name,
|
|
title: item.name
|
|
}))
|
|
.append($('<span>')
|
|
.addClass([ZBX_STYLE_BTN_ICON, ZBX_ICON_REMOVE_SMALLER])
|
|
.on('click', function() {
|
|
if (!ms.options.disabled && !item_disabled) {
|
|
removeSelected($obj, item.id);
|
|
if (isSearchFieldVisible($obj)) {
|
|
$('input[type="text"]', $obj)[0].focus({preventScroll:true});
|
|
}
|
|
|
|
$obj.trigger('change', ms);
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.on('click', function() {
|
|
if (isSearchFieldVisible($obj) && ms.options.selectedLimit != 1) {
|
|
$('.selected li.selected', $obj).removeClass('selected');
|
|
$(this).addClass('selected');
|
|
|
|
// preventScroll not work in IE.
|
|
$('input[type="text"]', $obj)[0].focus({preventScroll: true});
|
|
}
|
|
});
|
|
|
|
if (typeof item.inaccessible !== 'undefined' && item.inaccessible) {
|
|
$li.addClass('inaccessible');
|
|
}
|
|
|
|
if (item_disabled) {
|
|
$li.addClass('disabled');
|
|
}
|
|
|
|
$('.selected ul', $obj).append($li);
|
|
|
|
cleanSearch($obj);
|
|
}
|
|
|
|
function removeSelected($obj, id) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
$('.selected li[data-id]', $obj).each(function(){
|
|
if ($(this).data('id') == id) {
|
|
$(this).remove();
|
|
}
|
|
});
|
|
$('input', $obj).each(function() {
|
|
if ($(this).val() !== '' && $(this).val() == id) {
|
|
$(this).remove();
|
|
}
|
|
});
|
|
|
|
delete ms.values.selected[id];
|
|
|
|
if (!$('.selected li', $obj).length) {
|
|
addDefaultValue($obj);
|
|
}
|
|
|
|
cleanSearch($obj);
|
|
}
|
|
|
|
function addAvailable($obj, item) {
|
|
var ms = $obj.data('multiSelect'),
|
|
is_new = item.isNew || false,
|
|
prefix = item.prefix || '',
|
|
$li = $('<li>', {
|
|
'data-id': item.id,
|
|
'data-label': prefix + item.name
|
|
})
|
|
.on('mouseenter', function() {
|
|
$('li.suggest-hover', ms.values.available_div).removeClass('suggest-hover');
|
|
$li.addClass('suggest-hover');
|
|
})
|
|
.on('click', function() {
|
|
select($obj, item.id);
|
|
});
|
|
|
|
if (prefix !== '') {
|
|
$li.append($('<span>', {'class': 'grey', text: prefix}));
|
|
}
|
|
|
|
// Highlight matched.
|
|
if (ms.values.search !== item.name) {
|
|
var text = item.name.toLowerCase(),
|
|
search = ms.values.search.toLowerCase().replace(/[*]+/g, ''),
|
|
start = 0,
|
|
end = 0;
|
|
|
|
while (search !== '' && text.indexOf(search, end) > -1) {
|
|
end = text.indexOf(search, end);
|
|
|
|
if (end > start) {
|
|
$li.append(document.createTextNode(item.name.substring(start, end)));
|
|
}
|
|
|
|
$li.append($('<span>', {
|
|
class: !is_new ? 'suggest-found' : '',
|
|
text: item.name.substring(end, end + search.length)
|
|
})).toggleClass('suggest-new', is_new);
|
|
|
|
end += search.length;
|
|
start = end;
|
|
}
|
|
|
|
if (end < item.name.length) {
|
|
$li.append(document.createTextNode(item.name.substring(end, item.name.length)));
|
|
}
|
|
}
|
|
else {
|
|
$li.append($('<span>', {
|
|
class: !is_new ? 'suggest-found' : '',
|
|
text: item.name
|
|
})).toggleClass('suggest-new', is_new);
|
|
}
|
|
|
|
$('ul', ms.values.available_div).append($li);
|
|
}
|
|
|
|
function loadAvailable($obj) {
|
|
var ms = $obj.data('multiSelect'),
|
|
data = ms.values.searches[ms.values.cache_key];
|
|
|
|
cleanAvailable($obj);
|
|
|
|
var addNew = false;
|
|
|
|
if (ms.options.addNew && ms.values.search.length) {
|
|
if (data.length || objectSize(ms.values.selected) > 0) {
|
|
var names = {};
|
|
|
|
// Check if value exists among available values.
|
|
$.each(data, function(i, item) {
|
|
if (item.name === ms.values.search) {
|
|
names[item.name.toUpperCase()] = true;
|
|
}
|
|
});
|
|
|
|
if (typeof names[ms.values.search.toUpperCase()] === 'undefined') {
|
|
addNew = true;
|
|
}
|
|
|
|
// Check if value exists among selected values.
|
|
if (!addNew && objectSize(ms.values.selected) > 0) {
|
|
$.each(ms.values.selected, function(i, item) {
|
|
if (typeof item.isNew === 'undefined') {
|
|
names[item.name.toUpperCase()] = true;
|
|
}
|
|
else {
|
|
names[item.id.toUpperCase()] = true;
|
|
}
|
|
});
|
|
|
|
if (typeof names[ms.values.search.toUpperCase()] === 'undefined') {
|
|
addNew = true;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
addNew = true;
|
|
}
|
|
}
|
|
|
|
var available_more = false;
|
|
|
|
$.each(data, function(i, item) {
|
|
if (ms.options.limit == 0 || objectSize(ms.values.available) < ms.options.limit) {
|
|
if (typeof ms.values.available[item.id] === 'undefined'
|
|
&& typeof ms.values.selected[item.id] === 'undefined'
|
|
&& ms.options.excludeids.indexOf(item.id) === -1) {
|
|
ms.values.available[item.id] = item;
|
|
}
|
|
}
|
|
else {
|
|
available_more = true;
|
|
}
|
|
});
|
|
|
|
if (addNew) {
|
|
ms.values.available[ms.values.search] = {
|
|
id: ms.values.search,
|
|
name: ms.values.search + ' (' + ms.options.labels['new'] + ')',
|
|
isNew: true
|
|
};
|
|
}
|
|
|
|
var found = 0,
|
|
preselected = '';
|
|
|
|
if (objectSize(ms.values.available) == 0) {
|
|
var div = $('<div>', {
|
|
'class': 'multiselect-matches',
|
|
text: ms.options.labels['No matches found']
|
|
})
|
|
.on('click', function() {
|
|
$('input[type="text"]', $obj)[0].focus({preventScroll:true});
|
|
});
|
|
|
|
ms.values.available_div.append(div);
|
|
}
|
|
else {
|
|
ms.values.available_div.append($('<ul>', {
|
|
'class': 'multiselect-suggest',
|
|
'aria-hidden': true
|
|
}));
|
|
|
|
$.each(ms.values.available, function (i, item) {
|
|
if (found == 0) {
|
|
preselected = (item.prefix || '') + item.name;
|
|
}
|
|
addAvailable($obj, item);
|
|
found++;
|
|
});
|
|
}
|
|
|
|
if (found > 0) {
|
|
$('[aria-live]', $obj).text(
|
|
(available_more
|
|
? sprintf(t('More than %1$d matches for %2$s found'), found, ms.values.search)
|
|
: sprintf(t('%1$d matches for %2$s found'), found, ms.values.search)) +
|
|
', ' + sprintf(t('%1$s preselected, use down,up arrow keys and enter to select'), preselected)
|
|
);
|
|
}
|
|
else {
|
|
$('[aria-live]', $obj).text(ms.options.labels['No matches found']);
|
|
}
|
|
|
|
if (available_more) {
|
|
var div = $('<div>', {
|
|
'class': 'multiselect-matches',
|
|
text: ms.options.labels['More matches found...']
|
|
})
|
|
.on('click', function() {
|
|
$('input[type="text"]', $obj)[0].focus({preventScroll:true});
|
|
});
|
|
|
|
ms.values.available_div.prepend(div);
|
|
}
|
|
}
|
|
|
|
function showAvailable($obj) {
|
|
var ms = $obj.data('multiSelect'),
|
|
$available = ms.values.available_div;
|
|
|
|
if ($available.parent().is(document.body)) {
|
|
return;
|
|
}
|
|
|
|
$('.multiselect-available').not($available).each(function() {
|
|
hideAvailable($(this).data('obj'));
|
|
});
|
|
|
|
$available.data('obj', $obj);
|
|
|
|
// Will disconnect this handler in hideAvailable().
|
|
var hide_handler = function() {
|
|
hideAvailable($obj);
|
|
};
|
|
|
|
$available.data('hide_handler', hide_handler);
|
|
|
|
$obj.parents().add(window).one('scroll', hide_handler);
|
|
$(window).one('resize', hide_handler);
|
|
|
|
// For auto-test purposes.
|
|
$available.attr('data-opener', $obj.attr('id'));
|
|
|
|
var obj_offset = $obj.offset(),
|
|
obj_padding_y = $obj.outerHeight() - $obj.height(),
|
|
// Subtract 1px for borders of the input and available container to overlap.
|
|
available_top = obj_offset.top + $obj.height() + obj_padding_y / 2 - 1,
|
|
available_left = obj_offset.left,
|
|
available_width = $obj.width(),
|
|
available_max_height = Math.max(50, Math.min(400,
|
|
// Subtract 10px to make available box bottom clearly visible, for better usability.
|
|
$(window).height() + $(window).scrollTop() - available_top - obj_padding_y - 10
|
|
));
|
|
|
|
if (objectSize(ms.values.available) > 0) {
|
|
available_width_min = Math.max(available_width, 300);
|
|
|
|
// Prevent less than 15% width difference for the available list and the input field.
|
|
if ((available_width_min - available_width) / available_width > .15) {
|
|
available_width = available_width_min;
|
|
}
|
|
|
|
available_left = Math.min(available_left, $(window).width() + $(window).scrollLeft() - available_width);
|
|
if (available_left < 0) {
|
|
available_width += available_left;
|
|
available_left = 0;
|
|
}
|
|
}
|
|
|
|
$available.css({
|
|
'top': available_top,
|
|
'left': available_left,
|
|
'width': available_width,
|
|
'max-height': available_max_height
|
|
});
|
|
|
|
$available.scrollTop(0);
|
|
|
|
if (objectSize(ms.values.available) != 0) {
|
|
// Remove selected item selected state.
|
|
$('.selected li.selected', $obj).removeClass('selected');
|
|
|
|
// Pre-select first available item.
|
|
if ($('li', $available).length > 0) {
|
|
$('li.suggest-hover', $available).removeClass('suggest-hover');
|
|
$('li:first-child', $available).addClass('suggest-hover');
|
|
}
|
|
}
|
|
|
|
$available.appendTo(document.body);
|
|
}
|
|
|
|
function hideAvailable($obj) {
|
|
var ms = $obj.data('multiSelect'),
|
|
$available = ms.values.available_div;
|
|
|
|
clearSearchTimeout($obj);
|
|
|
|
if (!$available.parent().is(document.body)) {
|
|
return;
|
|
}
|
|
|
|
$available.detach();
|
|
|
|
var hide_handler = $available.data('hide_handler');
|
|
$obj.parents().add(window).off('scroll', hide_handler);
|
|
$(window).off('resize', hide_handler);
|
|
|
|
$available
|
|
.removeData(['obj', 'hide_handler'])
|
|
.removeAttr('data-opener');
|
|
}
|
|
|
|
function cleanAvailable($obj) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
hideAvailable($obj);
|
|
|
|
ms.values.available = {};
|
|
ms.values.available_div.empty();
|
|
}
|
|
|
|
function scrollAvailable($obj) {
|
|
var ms = $obj.data('multiSelect'),
|
|
$available = ms.values.available_div,
|
|
$selected = $available.find('li.suggest-hover');
|
|
|
|
if ($selected.length > 0) {
|
|
var available_height = $available.height(),
|
|
selected_top = 0,
|
|
selected_height = $selected.outerHeight(true);
|
|
|
|
if ($('.multiselect-matches', $available)) {
|
|
selected_top += $('.multiselect-matches', $available).outerHeight(true);
|
|
}
|
|
|
|
$available.find('li').each(function() {
|
|
var item = $(this);
|
|
if (item.hasClass('suggest-hover')) {
|
|
return false;
|
|
}
|
|
selected_top += item.outerHeight(true);
|
|
});
|
|
|
|
if (selected_top < $available.scrollTop()) {
|
|
var prev = $selected.prev();
|
|
|
|
$available.scrollTop((prev.length == 0) ? 0 : selected_top);
|
|
}
|
|
else if (selected_top + selected_height > $available.scrollTop() + available_height) {
|
|
$available.scrollTop(selected_top - available_height + selected_height);
|
|
}
|
|
}
|
|
else {
|
|
$available.scrollTop(0);
|
|
}
|
|
}
|
|
|
|
function cleanSearchInput($obj) {
|
|
$('input[type="text"]', $obj).val('');
|
|
|
|
clearSearchTimeout($obj);
|
|
}
|
|
|
|
function cleanSearchHistory($obj) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
ms.values.cache_key = '';
|
|
ms.values.search = '';
|
|
ms.values.searches = {};
|
|
ms.values.searching = {};
|
|
}
|
|
|
|
function clearSearchTimeout($obj) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
if (ms.values.searchTimeout !== undefined) {
|
|
clearTimeout(ms.values.searchTimeout);
|
|
delete ms.values.searchTimeout;
|
|
}
|
|
}
|
|
|
|
function cleanSearch($obj) {
|
|
cleanAvailable($obj);
|
|
cleanSearchInput($obj);
|
|
cleanSearchHistory($obj);
|
|
updateSearchFieldVisibility($obj);
|
|
}
|
|
|
|
function updateSearchFieldVisibility($obj) {
|
|
var ms = $obj.data('multiSelect'),
|
|
visible_now = !$obj.hasClass('search-disabled'),
|
|
visible = !ms.options.disabled
|
|
&& (ms.options.selectedLimit == 0 || $('.selected li', $obj).length < ms.options.selectedLimit);
|
|
|
|
if (visible === visible_now) {
|
|
return;
|
|
}
|
|
|
|
if (visible) {
|
|
var $label = $('label[for=' + $obj.attr('id') + '_ms]');
|
|
|
|
$obj.removeClass('search-disabled')
|
|
.find('input[type="text"]')
|
|
.attr({
|
|
placeholder: ms.options.placeholder,
|
|
'aria-label': ($label.length ? $label.text() + '. ' : '') + ms.options.placeholder,
|
|
readonly: false
|
|
});
|
|
}
|
|
else {
|
|
$obj.addClass('search-disabled')
|
|
.find('input[type="text"]')
|
|
.attr({
|
|
placeholder: '',
|
|
'aria-label': '',
|
|
readonly: true
|
|
});
|
|
}
|
|
}
|
|
|
|
function isSearchFieldVisible($obj) {
|
|
return !$obj.hasClass('search-disabled');
|
|
}
|
|
|
|
function getLimit($obj) {
|
|
var ms = $obj.data('multiSelect');
|
|
|
|
return (ms.options.limit != 0)
|
|
? ms.options.limit + objectSize(ms.values.selected) + ms.options.excludeids.length + 1
|
|
: null;
|
|
}
|
|
})(jQuery);
|