scinote-web/app/assets/javascripts/sitewide/dropdown_selector.js

1110 lines
39 KiB
JavaScript
Raw Normal View History

2019-08-26 21:49:33 +08:00
/* global PerfectScrollbar activePSB PerfectSb I18n */
2019-08-07 16:25:58 +08:00
/* eslint-disable no-unused-vars, no-use-before-define */
2019-11-21 19:19:47 +08:00
/*
Data options for SELECT:
data-ajax-url // Url for GET ajax request
data-select-by-group // Add groups to dropdown
data-disable-placeholder // Placeholder for disabled fields
data-placeholder // Search placeholder
data-select-hint // A hint on top of a dropdown
data-disable-on-load // Disable input after initialization
data-select-all-button // Text for select all button
2019-12-06 20:18:35 +08:00
data-combine-tags // Combine multiple tags to one (in simple mode gives you multiple select)
data-select-multiple-all-selected // Text for combine tags, when all selected
data-select-multiple-name // Text for combine tags, when select more than one tag
data-view-mode // Run in view mode
Initialization
dropdownSelector.init('#select-element', config)
config = {
localFilter: function(data), // Filter non-AJAX data
optionLabel: function(option), // Change option label
optionClass: string, // Add class to option
optionStyle: string, // Add style to option
tagLabel: function(tag), // Change tag label (only for tags)
tagClass: string, // Add class to tag (only for tags)
tagStyle: string, // Add style to tag (only for tags)
ajaxParams: function(params), // Customize params to AJAX request
onOpen: function(), // Run action on open options container
onClose: function(), // Run action on close options container
onSelect: function(), // Run action after select
onChange: function(), // Run action after change
onUnSelect: function(), // Run action after unselect
customDropdownIcon: function(), // Add custom dropdown icon
inputTagMode: boolean, // Use as simple input tag field
2019-11-24 04:09:34 +08:00
selectKeys: array, // array of keys id which use for fast select // default - [13]
noEmptyOption: boolean, // use defaut select (only for single option select). default 'false'
singleSelect: boolean, // disable multiple select. default 'false'
selectAppearance: string, // 'tag' or 'simple'. Default 'tag'
closeOnSelect: boolean, // Close dropdown after select
disableSearch: boolean, // Disable search
2020-02-07 21:57:07 +08:00
emptyOptionAjax: boolean, // Add empty option for ajax request
labelHTML: bolean, // render as HTMLelement or text
}
*/
2019-08-02 21:57:41 +08:00
var dropdownSelector = (function() {
2019-08-07 16:25:58 +08:00
// /////////////////////
// Support functions //
// ////////////////////
const MAX_DROPDOWN_HEIGHT = 320;
2019-08-07 16:25:58 +08:00
// Change direction of dropdown depends of container position
function updateDropdownDirection(selector, container) {
var windowHeight = $(window).height();
var containerPositionTop = container[0].getBoundingClientRect().top;
2020-01-13 21:46:43 +08:00
var containerPositionLeft = container[0].getBoundingClientRect().left;
2019-08-07 16:25:58 +08:00
var containerHeight = container[0].getBoundingClientRect().height;
var containerWidth = container[0].getBoundingClientRect().width;
var bottomSpace;
var modalContainer = container.closest('.modal-dialog');
var modalContainerBottom = 0;
var modalContainerTop = 0;
var maxHeight = 0;
const bottomThreshold = 280;
if (modalContainer.length && windowHeight !== modalContainer.height()) {
2020-05-11 20:53:28 +08:00
let modalClientRect = modalContainer[0].getBoundingClientRect();
windowHeight = modalContainer.height() + modalClientRect.top;
containerPositionLeft -= modalClientRect.left;
modalContainerBottom = $(window).height() - modalClientRect.bottom;
modalContainerTop = modalClientRect.top;
2019-12-11 21:49:14 +08:00
maxHeight += modalContainerBottom;
}
bottomSpace = windowHeight - containerPositionTop - containerHeight;
2019-12-11 21:49:14 +08:00
if ((modalContainerBottom + bottomSpace) < bottomThreshold) {
2019-08-07 16:25:58 +08:00
container.addClass('inverse');
maxHeight = Math.min(containerPositionTop - 122 + maxHeight, MAX_DROPDOWN_HEIGHT);
container.find('.dropdown-container').css('max-height', `${maxHeight}px`)
.css('margin-bottom', `${((containerPositionTop - modalContainerTop) * -1)}px`)
2020-01-13 21:46:43 +08:00
.css('left', `${containerPositionLeft}px`)
2019-08-07 16:25:58 +08:00
.css('width', `${containerWidth}px`);
} else {
container.removeClass('inverse');
maxHeight = Math.min(bottomSpace - 32 + maxHeight, MAX_DROPDOWN_HEIGHT);
container.find('.dropdown-container').css('max-height', `${maxHeight}px`)
.css('width', `${containerWidth}px`)
2020-01-13 21:46:43 +08:00
.css('left', `${containerPositionLeft}px`)
2019-12-11 21:49:14 +08:00
.css('margin-top', `${(bottomSpace * -1)}px`);
2019-08-07 16:25:58 +08:00
}
}
// Get data in JSON from field
function getCurrentData(container) {
if (!container.find('.data-field').val()) {
return '';
}
2019-08-07 16:25:58 +08:00
return JSON.parse(container.find('.data-field').val());
}
// Save data to the field
function updateCurrentData(container, data) {
container.find('.data-field').val(JSON.stringify(data)).change();
2019-08-07 16:25:58 +08:00
}
// Search filter for non-ajax data
function filterOptions(selector, container, options) {
var customFilter = selector.data('config').localFilter;
2019-08-26 21:49:33 +08:00
var searchQuery = container.find('.search-field').val();
var data = customFilter ? customFilter(options) : options;
if (searchQuery.length === 0) return data;
return $.grep(data, (n) => {
2019-08-07 16:25:58 +08:00
return n.label.toLowerCase().includes(searchQuery.toLowerCase());
});
}
// Check if all options selected, for non ajax data
function allOptionsSelected(selector, container) {
return JSON.parse(container.find('.data-field').val()).length === selector.find('option').length && !(selector.data('ajax-url'));
}
// Update dropdown selection, based on save data
function refreshDropdownSelection(selector, container) {
container.find('.dropdown-option, .dropdown-group').removeClass('select');
$.each(getCurrentData(container), function(i, selectedOption) {
container.find(`.dropdown-option[data-value="${_.escape(selectedOption.value)}"][data-group="${selectedOption.group || ''}"]`)
2019-08-07 16:25:58 +08:00
.addClass('select');
});
if (selector.data('select-by-group')) {
$.each(container.find('.dropdown-group'), function(gi, group) {
if ($(group).find('.dropdown-option').length === $(group).find('.dropdown-option.select').length) {
$(group).addClass('select');
}
});
}
}
function enableViewMode(selector, container) {
container
.addClass('view-mode disabled')
.removeClass('open')
.find('.search-field').prop('disabled', true);
}
2019-08-26 21:49:33 +08:00
function disableEnableDropdown(selector, container, mode) {
var searchFieldValue = container.find('.search-field');
if (mode) {
if ($(selector).data('ajax-url')) {
updateCurrentData(container, []);
}
updateTags(selector, container, { skipChange: true });
2019-12-06 20:18:35 +08:00
searchFieldValue.attr('placeholder', selector.data('disable-placeholder') || '');
container.addClass('disabled').removeClass('open')
2019-08-26 21:49:33 +08:00
.find('.search-field').val('')
.prop('disabled', true);
} else {
container.removeClass('disabled')
2019-08-26 21:49:33 +08:00
.find('.search-field').prop('disabled', false);
updateTags(selector, container, { skipChange: true });
}
}
2019-10-17 16:06:40 +08:00
// Read option to JSON
function convertOptionToJson(option) {
if (option === undefined) {
return { label: '', value: '', params: {} };
}
2019-10-17 16:06:40 +08:00
return {
2019-10-18 17:31:13 +08:00
label: option.innerHTML,
value: option.value,
group: option.dataset.group || '',
params: JSON.parse(option.dataset.params || '{}')
2019-10-18 17:31:13 +08:00
};
2019-10-17 16:06:40 +08:00
}
function noOptionsForSelect(selector) {
return !$(selector).data('ajax-url') && $(selector).find('.dropdown-option').length == 0;
}
2019-10-17 16:06:40 +08:00
// Ajax intial values, we will use default options //
function ajaxInitialValues(selector, container) {
2019-10-18 17:31:13 +08:00
var intialData = [];
2019-10-17 16:06:40 +08:00
$(selector).find('option').each((i, option) => {
2019-10-18 17:31:13 +08:00
intialData.push(convertOptionToJson(option));
});
updateCurrentData(container, intialData);
2019-10-17 16:06:40 +08:00
updateTags(selector, container, { skipChange: true });
}
// Add selected option to value
2020-08-12 19:54:05 +08:00
function addSelectedOptions(selector, container) {
var selectedOptions = [];
var optionSelector = selector.data('config').noEmptyOption ? 'option:selected' : 'option[data-selected=true]';
$.each($(selector).find(optionSelector), function(i, option) {
selectedOptions.push(convertOptionToJson(option));
if (selector.data('config').singleSelect) return false;
return true;
});
if (!selectedOptions.length) return false;
setData(selector, selectedOptions, true);
return true;
}
2019-10-18 17:31:13 +08:00
// Prepare custom dropdown icon
function prepareCustomDropdownIcon(selector, config) {
if (config.inputTagMode && noOptionsForSelect(selector)) {
return '';
}
2019-10-18 17:31:13 +08:00
if (config.customDropdownIcon) {
return config.customDropdownIcon();
}
2023-06-08 23:33:50 +08:00
return '<i class="sn-icon sn-icon-down right-icon"></i><i class="sn-icon sn-icon-search right-icon simple-dropdown"></i>';
2019-10-18 17:31:13 +08:00
}
// Set new data
function setData(selector, data, skipSelect) {
updateCurrentData(selector.next(), data);
refreshDropdownSelection(selector, selector.next());
updateTags(selector, selector.next(), { select: true, skipSelect: skipSelect });
}
// Delete specific value
function deleteValue(selector, container, value, group = '', skipUnselect = false) {
var selectedOptions = getCurrentData(container);
var toDelete = selectedOptions.findIndex(x => (String(x.value) === String(value)
&& (String(x.group) === String(group) || !selector.data('select-by-group'))
));
selectedOptions.splice(toDelete, 1);
updateCurrentData(container, selectedOptions);
updateTags(selector, container, { unselect: true, tagId: value, skipUnselect: skipUnselect });
}
// Function generate new tag
function addNewTag(selector, container) {
var searchField = container.find('.search-field');
var selectArray = getCurrentData(container);
var newTag = {
label: searchField.val(),
value: searchField.val()
};
$.each(selectArray, function() {
if (this.value === newTag.value) searchField.val('');
});
if (searchField.val().length <= 1) return;
selectArray.push(newTag);
searchField.val('');
updateCurrentData(container, selectArray);
updateTags(selector, container, { select: true });
}
// initialize keyboard control
function initKeyboardControl(selector, container) {
2019-11-24 04:09:34 +08:00
container.find('.search-field').keydown(function(e) {
var dropdownContainer = container.find('.dropdown-container');
var pressedKey = e.keyCode;
var selectedOption = dropdownContainer.find('.highlight');
if (selectedOption.length === 0 && (pressedKey === 38 || pressedKey === 40)) {
dropdownContainer.find('.dropdown-option').first().addClass('highlight');
}
if (pressedKey === 38) { // arrow up
2019-11-24 04:09:34 +08:00
if (selectedOption.prev('.dropdown-option').length) {
selectedOption.removeClass('highlight').prev().addClass('highlight');
}
if (selectedOption.prev('.delimiter').length) {
selectedOption.removeClass('highlight').prev().prev().addClass('highlight');
2019-11-24 04:09:34 +08:00
}
} else if (pressedKey === 40) { // arrow down
2019-11-24 04:09:34 +08:00
if (selectedOption.next('.dropdown-option').length) {
selectedOption.removeClass('highlight').next().addClass('highlight');
}
if (selectedOption.next('.delimiter').length) {
selectedOption.removeClass('highlight').next().next().addClass('highlight');
2019-11-24 04:09:34 +08:00
}
} else if (pressedKey === 8 && e.target.value === '') { // backspace
2023-06-20 21:53:33 +08:00
deleteTag(selector, container, container.find('.ds-tags .sn-icon-close-small').last());
2019-11-24 04:09:34 +08:00
}
});
}
2019-08-07 16:25:58 +08:00
// //////////////////////
// Private functions ///
// /////////////////////
// Initialization of dropdown
function generateDropdown(selector, config = {}) {
var selectElement = $(selector);
2019-08-05 23:21:58 +08:00
var optionContainer;
2019-08-26 21:49:33 +08:00
var perfectScroll;
var dropdownContainer;
var toggleElement;
2019-12-02 23:04:50 +08:00
if (selectElement.length === 0) return;
// Check if element exist or already initialized
2019-12-02 23:04:50 +08:00
if (selectElement.next().hasClass('dropdown-selector-container')) selectElement.next().remove();
// Create initial container after select block
dropdownContainer = selectElement.after('<div class="dropdown-selector-container"></div>').next();
2019-08-06 21:25:52 +08:00
// Save config info to select element
2019-08-07 16:25:58 +08:00
selectElement.data('config', config);
2019-08-06 21:25:52 +08:00
// Draw main elements
2019-08-02 21:57:41 +08:00
$(`
<div class="dropdown-container"></div>
2019-08-06 21:25:52 +08:00
<div class="input-field">
<input type="text" class="search-field" data-options-selected=0 placeholder="${selectElement.data('placeholder') || ''}"></input>
${prepareCustomDropdownIcon(selector, config)}
2019-08-06 21:25:52 +08:00
</div>
<input type="hidden" class="data-field" value="[]">
2019-08-07 16:25:58 +08:00
`).appendTo(dropdownContainer);
2019-08-02 21:57:41 +08:00
// Blank option
if (selectElement.data('blank')) {
$(`<div class="dropdown-blank btn">${selectElement.data('blank')}</div>`)
.appendTo(dropdownContainer.find('.dropdown-container'))
.click(() => {
dropdownContainer.find('.dropdown-group, .dropdown-option').removeClass('select');
saveData(selectElement, dropdownContainer);
dropdownContainer.removeClass('open');
});
}
if (selectElement.data('toggle-target')) {
dropdownContainer.find('.data-field').on('change', function() {
toggleElement = $(selectElement.data('toggle-target'));
if (getCurrentData(dropdownContainer).length > 0) {
toggleElement.removeClass('hidden');
toggleElement.find('input, select').removeAttr('disabled');
} else {
toggleElement.addClass('hidden');
toggleElement.find('input, select').attr('disabled', true);
}
});
}
// If we setup Select All we draw it and add correspond logic
2019-08-07 16:25:58 +08:00
if (selectElement.data('select-all-button')) {
2019-08-06 21:25:52 +08:00
$(`<div class="dropdown-select-all btn">${selectElement.data('select-all-button')}</div>`)
.appendTo(dropdownContainer.find('.dropdown-container'))
.click(() => {
// For AJAX dropdown we will use only "Deselect All"
2019-08-06 21:25:52 +08:00
if (allOptionsSelected(selectElement, dropdownContainer) || selectElement.data('ajax-url')) {
2019-08-07 16:25:58 +08:00
dropdownContainer.find('.dropdown-group, .dropdown-option').removeClass('select');
2019-08-06 21:25:52 +08:00
} else {
2019-08-07 16:25:58 +08:00
dropdownContainer.find('.dropdown-group, .dropdown-option').addClass('select');
2019-08-06 21:25:52 +08:00
}
2019-08-07 16:25:58 +08:00
saveData(selectElement, dropdownContainer);
});
2019-08-06 21:25:52 +08:00
}
2019-10-17 16:06:40 +08:00
if (selectElement.data('ajax-url') || config.inputTagMode) {
// If we use AJAX dropdown or tags input, options become initial values
2019-10-18 17:31:13 +08:00
ajaxInitialValues(selectElement, dropdownContainer);
2019-10-17 16:06:40 +08:00
}
// When we start typing it will dynamically update data
dropdownContainer.find('.search-field')
.keyup((e) => {
2019-11-24 04:09:34 +08:00
if (e.keyCode === 38
|| e.keyCode === 40
|| (config.selectKeys || []).includes(e.keyCode)) {
return;
}
e.stopPropagation();
2019-08-07 16:25:58 +08:00
loadData(selectElement, dropdownContainer);
})
.keypress((e) => {
2019-11-24 04:09:34 +08:00
if ((config.selectKeys || [13]).includes(e.keyCode)) {
if (config.inputTagMode) {
addNewTag(selectElement, dropdownContainer);
2019-11-24 04:09:34 +08:00
} else if (dropdownContainer.find('.highlight').length) {
dropdownContainer.find('.highlight').click();
} else {
dropdownContainer.find('.dropdown-option').first().click();
}
dropdownContainer.find('.search-field').val('');
e.stopPropagation();
e.preventDefault();
}
}).click((e) =>{
e.stopPropagation();
if (dropdownContainer.hasClass('open')) {
loadData(selectElement, dropdownContainer);
} else {
dropdownContainer.find('.input-field').click();
}
});
2019-08-06 21:25:52 +08:00
// Initialize scroll bar inside options container
2019-08-26 21:49:33 +08:00
perfectScroll = new PerfectScrollbar(dropdownContainer.find('.dropdown-container')[0]);
activePSB.push(perfectScroll);
2019-08-02 21:57:41 +08:00
// Select options container
2019-08-07 16:25:58 +08:00
optionContainer = dropdownContainer.find('.dropdown-container');
2019-08-05 23:21:58 +08:00
dropdownContainer.find('.input-field').on('focus', () => {
setTimeout(function() {
if (!dropdownContainer.hasClass('open')) {
dropdownContainer.find('.input-field').click();
}
}, 250);
});
dropdownContainer.find('.search-field').on('keydown', function(e) {
if (e.which === 9) { // Tab key
dropdownContainer.find('.search-field').val('');
if (dropdownContainer.hasClass('open') && config.onClose) {
config.onClose();
}
dropdownContainer.removeClass('open active');
}
});
// Add click event to input field
2019-08-02 21:57:41 +08:00
dropdownContainer.find('.input-field').click(() => {
// Now we can have only one dropdown opened at same time
2019-08-07 16:25:58 +08:00
$('.dropdown-selector-container').removeClass('active');
dropdownContainer.addClass('active');
$('.dropdown-selector-container:not(.active)').removeClass('open');
// If dropdown disabled or we use it in only tag mode we not open it
if (dropdownContainer.hasClass('disabled') || (config.inputTagMode && noOptionsForSelect(selector))) return;
// Each time we open option contianer we must scroll it
2019-08-07 16:25:58 +08:00
optionContainer.scrollTo(0);
// Now open/close option container
2019-08-07 16:25:58 +08:00
dropdownContainer.toggleClass('open');
if (dropdownContainer.hasClass('open')) {
// on Open we load new data
2019-08-07 16:25:58 +08:00
loadData(selectElement, dropdownContainer);
updateDropdownDirection(selectElement, dropdownContainer);
2019-09-12 20:11:27 +08:00
dropdownContainer.find('.search-field').focus();
2019-10-18 17:31:13 +08:00
// onOpen event
if (config.onOpen) {
config.onOpen();
}
2019-10-17 16:06:40 +08:00
} else {
// on Close we blur search field
dropdownContainer.find('.search-field').blur().val('');
2019-10-18 17:31:13 +08:00
// onClose event
if (config.onClose) {
config.onClose();
}
2019-08-02 21:57:41 +08:00
}
2019-08-07 16:25:58 +08:00
});
// When user will resize browser we must check dropdown position
2019-12-11 21:49:14 +08:00
$(window).resize(() => { updateDropdownDirection(selectElement, dropdownContainer); });
$(window).scroll(() => { updateDropdownDirection(selectElement, dropdownContainer); });
// When user will click away, we must close dropdown
2019-10-18 17:36:56 +08:00
$(window).click(() => {
if (dropdownContainer.hasClass('open')) {
2019-11-24 04:09:34 +08:00
dropdownContainer.find('.search-field').val('');
}
if (dropdownContainer.hasClass('open') && config.onClose) {
2019-10-18 17:31:13 +08:00
config.onClose();
}
dropdownContainer.removeClass('open active');
2019-10-18 17:31:13 +08:00
});
// Prevent closing dropdown if we click inside
dropdownContainer.click((e) => { e.stopPropagation(); });
2019-08-02 21:57:41 +08:00
// Hide original select element
2019-08-07 16:25:58 +08:00
selectElement.css('display', 'none');
// Disable dropdown by default
2019-08-26 21:49:33 +08:00
if (selectElement.data('disable-on-load')) disableEnableDropdown(selectElement, dropdownContainer, true);
// EnableView mode
if (selectElement.data('view-mode')) {
enableViewMode(selectElement, dropdownContainer);
}
// Select default value
2020-08-12 19:54:05 +08:00
if (!selectElement.data('ajax-url')) {
2020-08-27 17:30:55 +08:00
addSelectedOptions(selectElement, dropdownContainer);
}
// Enable simple mode for dropdown selector
if (config.selectAppearance === 'simple') {
dropdownContainer.addClass('simple-mode');
if (dropdownContainer.find('.tag-label').length) {
dropdownContainer.find('.search-field').attr('placeholder', dropdownContainer.find('.tag-label').text().trim());
}
}
// Disable search
if (config.disableSearch) {
dropdownContainer.addClass('disable-search');
}
// initialization keyboard control
initKeyboardControl(selector, dropdownContainer);
2019-11-24 04:09:34 +08:00
// In some case dropdown position not correctly calculated
2019-08-07 16:25:58 +08:00
updateDropdownDirection(selectElement, dropdownContainer);
2019-08-02 21:57:41 +08:00
}
2019-08-07 16:25:58 +08:00
// Load data to dropdown list
2019-08-06 21:25:52 +08:00
function loadData(selector, container, ajaxData = null) {
2019-08-07 16:25:58 +08:00
var data;
2019-08-09 20:31:50 +08:00
var containerDropdown = container.find('.dropdown-container');
// We need to remeber previos option container size before update
2019-08-09 20:31:50 +08:00
containerDropdown.css('height', `${containerDropdown.height()}px`);
2019-08-06 21:25:52 +08:00
if (ajaxData) {
// For ajax we simpy use data from request
2019-08-07 16:25:58 +08:00
data = ajaxData;
2019-08-06 21:25:52 +08:00
} else {
// For local from select options
2019-08-07 16:25:58 +08:00
data = dataSource(selector, container);
2019-08-06 21:25:52 +08:00
}
2019-08-07 16:25:58 +08:00
// Draw option object
2019-10-18 17:31:13 +08:00
function drawOption(selector2, option, group = null) {
// Check additional params from config
var params = option.params || {};
2019-10-18 17:31:13 +08:00
var customLabel = selector2.data('config').optionLabel;
var customClass = params.optionClass || selector2.data('config').optionClass || '';
2019-10-18 17:31:13 +08:00
var customStyle = selector2.data('config').optionStyle;
var optionElement = $(`
<div class="dropdown-option ${customClass}" style="${customStyle ? customStyle(option) : ''}">
</div>
2019-08-07 16:25:58 +08:00
`);
optionElement
.attr('title', (option.params && option.params.tooltip) || '')
.attr('data-params', JSON.stringify(option.params || {}))
.attr('data-label', option.label)
.attr('data-group', group ? group.value : '')
.attr('data-value', option.value);
if (customLabel) {
optionElement.html(customLabel(option));
} else {
optionElement.html(option.label);
}
return optionElement;
2019-08-05 23:21:58 +08:00
}
// Draw delimiter object
function drawDelimiter() {
return $('<div class="delimiter"></div>');
}
2019-08-07 16:25:58 +08:00
// Draw group object
2019-08-05 23:21:58 +08:00
function drawGroup(group) {
return $(`
<div class="dropdown-group">
<div class="group-name checkbox-icon">${group.label}</div>
</div>
2019-08-07 16:25:58 +08:00
`);
2019-08-05 23:21:58 +08:00
}
2019-08-07 16:25:58 +08:00
// Click action for option object
2019-08-05 23:21:58 +08:00
function clickOption() {
var $container = $(this).closest('.dropdown-selector-container');
// Unselect all previous one if single select
if (selector.data('config').singleSelect) {
$container.find('.dropdown-option').removeClass('select');
updateCurrentData($container, []);
selector.val($(this).data('value')).change();
}
2019-08-07 16:25:58 +08:00
$(this).toggleClass('select');
saveData(selector, $container);
2019-08-05 23:21:58 +08:00
}
2019-08-06 21:25:52 +08:00
// Remove placeholder from option container
container.find('.dropdown-group, .dropdown-option, .empty-dropdown, .dropdown-hint, .delimiter').remove();
2019-08-26 21:49:33 +08:00
if (!data) return;
2020-02-17 17:47:10 +08:00
if (data.length > 0 && !(data.length === 1 && data[0].value === '')) {
if (selector.data('select-hint')) {
$(`<div class="dropdown-hint">${selector.data('select-hint')}</div>`)
.appendTo(container.find('.dropdown-container'));
}
// If we use select-by-group option we need first draw groups
2019-08-26 21:49:33 +08:00
if (selector.data('select-by-group')) {
$.each(data, function(gi, group) {
// First we create our group
2019-08-26 21:49:33 +08:00
var groupElement = drawGroup(group);
// Now add options to this group
2019-08-26 21:49:33 +08:00
$.each(group.options, function(oi, option) {
2019-10-17 16:06:40 +08:00
var optionElement = drawOption(selector, option, group);
2019-08-26 21:49:33 +08:00
optionElement.click(clickOption);
optionElement.appendTo(groupElement);
});
// Now for each group we add action to select all options
2019-08-26 21:49:33 +08:00
groupElement.find('.group-name').click(function() {
var groupContainer = $(this).parent();
// Disable group select to single select
if (selector.data('config').singleSelect) return;
2019-08-26 21:49:33 +08:00
if (groupContainer.toggleClass('select').hasClass('select')) {
groupContainer.find('.dropdown-option').addClass('select');
} else {
groupContainer.find('.dropdown-option').removeClass('select');
}
saveData(selector, container);
});
// And finally appen group to option container
2019-08-26 21:49:33 +08:00
groupElement.appendTo(container.find('.dropdown-container'));
2019-08-07 16:25:58 +08:00
});
2019-08-26 21:49:33 +08:00
} else {
// For simple options we only draw them
2019-08-26 21:49:33 +08:00
$.each(data, function(oi, option) {
var optionElement;
if (option.delimiter || (option.params && option.params.delimiter)) {
drawDelimiter().appendTo(container.find('.dropdown-container'));
return;
}
optionElement = drawOption(selector, option);
2019-08-26 21:49:33 +08:00
optionElement.click(clickOption);
optionElement.appendTo(container.find('.dropdown-container'));
2019-08-07 16:25:58 +08:00
});
2019-08-26 21:49:33 +08:00
}
2019-08-02 21:57:41 +08:00
} else {
// If we data empty, draw placeholder
2019-08-26 21:49:33 +08:00
$(`<div class="empty-dropdown">${I18n.t('dropdown_selector.nothing_found')}</div>`).appendTo(container.find('.dropdown-container'));
2019-08-02 21:57:41 +08:00
}
// Update scrollbar
2019-08-07 16:25:58 +08:00
PerfectSb().update_all();
// Check position of option dropdown
2019-08-07 16:25:58 +08:00
refreshDropdownSelection(selector, container);
// Unfreeze option container height
2019-08-09 20:31:50 +08:00
containerDropdown.css('height', 'auto');
2019-08-02 21:57:41 +08:00
}
2019-08-07 16:25:58 +08:00
// Save data to local field
2019-08-05 23:21:58 +08:00
function saveData(selector, container) {
// Check what we have now selected
2019-08-07 16:25:58 +08:00
var selectArray = getCurrentData(container);
// Search option by value and group
2019-08-07 16:25:58 +08:00
function findOption(options, option) {
return options.findIndex(x => (x.value === option.dataset.value
&& x.group === option.dataset.group));
}
// First we clear search field
if (selector.data('config').singleSelect) container.find('.search-field').val('');
2019-08-07 16:25:58 +08:00
// Now we check all options in dropdown for selection and add them to array
2019-08-07 16:25:58 +08:00
$.each(container.find('.dropdown-container .dropdown-option'), function(oi, option) {
2019-08-06 21:25:52 +08:00
var alreadySelected;
var toDelete;
var newOption;
2019-08-07 16:25:58 +08:00
if ($(option).hasClass('select')) {
alreadySelected = findOption(selectArray, option);
// If it is new option we add it
2019-08-07 16:25:58 +08:00
if (alreadySelected === -1) {
2019-08-06 21:25:52 +08:00
newOption = {
label: option.dataset.label,
value: option.dataset.value,
2019-10-17 16:06:40 +08:00
group: option.dataset.group,
params: JSON.parse(option.dataset.params)
2019-08-07 16:25:58 +08:00
};
selectArray.push(newOption);
2019-08-06 21:25:52 +08:00
}
} else {
// If we deselect option we remove it
2019-08-07 16:25:58 +08:00
toDelete = findOption(selectArray, option);
if (toDelete >= 0) selectArray.splice(toDelete, 1);
2019-08-06 21:25:52 +08:00
}
// This complex required to save order of tags
2019-08-07 16:25:58 +08:00
});
// Now we save new data
2019-08-07 16:25:58 +08:00
updateCurrentData(container, selectArray);
// Redraw tags
2019-10-18 17:31:13 +08:00
updateTags(selector, container, { select: true });
2019-08-05 23:21:58 +08:00
}
function deleteTag(selector, container, target) {
var tagLabel = target.prev();
// Start delete animation
target.parent().addClass('closing');
// Add timeout for deleting animation
setTimeout(() => {
if (selector.data('combine-tags')) {
// if we use combine-tags options we simply clear all values
container.find('.data-field').val('[]');
updateTags(selector, container);
} else {
// Or delete specific one
deleteValue(selector, container, tagLabel.data('ds-tag-id'), tagLabel.data('ds-tag-group'));
if (selector.data('config').tagClass) {
removeOptionFromSelector(selector, tagLabel.data('ds-tag-id'));
}
}
}, 350);
}
2019-08-07 16:25:58 +08:00
// Refresh tags in input field
function updateTags(selector, container, config = {}) {
2019-08-07 16:25:58 +08:00
var selectedOptions = getCurrentData(container);
2019-08-26 21:49:33 +08:00
var searchFieldValue = container.find('.search-field');
2019-08-05 23:21:58 +08:00
2019-08-07 16:25:58 +08:00
// Draw tag and assign event
2019-08-06 21:25:52 +08:00
function drawTag(data) {
// Check for custom options
2019-08-07 16:25:58 +08:00
var customLabel = selector.data('config').tagLabel;
2019-10-17 16:06:40 +08:00
var customClass = selector.data('config').tagClass || '';
var customStyle = selector.data('config').tagStyle;
// Select element appearance
var tagAppearance = selector.data('config').selectAppearance === 'simple' ? 'ds-simple' : 'ds-tags';
2019-11-27 21:04:59 +08:00
var label = customLabel ? customLabel(data) : data.label;
var title = (data.params && data.params.tooltip) || $('<span>' + label + '</span>').text().trim();
// Add new tag before search field
var tag = $(`<div class="${tagAppearance} ${customClass}" style="${customStyle ? customStyle(data) : ''}" >
<div class="tag-label">
2019-08-06 21:25:52 +08:00
</div>
2023-06-20 21:53:33 +08:00
<i class="sn-icon sn-icon-close-small ${selector.data('config').singleSelect ? 'hidden' : ''}"></i>
2019-08-26 21:49:33 +08:00
</div>`).insertBefore(container.find('.input-field .search-field'));
tag.find('.tag-label')
.attr('data-ds-tag-group', data.group)
.attr('data-ds-tag-id', data.value)
.attr('title', title);
if (selector.data('config').labelHTML) {
tag.find('.tag-label').html(label);
} else {
tag.find('.tag-label').text(label);
}
// Now we need add delete action to tag
2023-06-20 21:53:33 +08:00
tag.find('.sn-icon-close-small').click(function(e) {
2019-08-07 16:25:58 +08:00
e.stopPropagation();
deleteTag(selector, container, $(this));
2019-08-05 23:21:58 +08:00
});
}
// Clear all tags
container.find('.ds-tags, .ds-simple').remove();
2019-08-07 16:25:58 +08:00
if (selector.data('combine-tags')) {
// If we use combine-tags options we draw only one tag
2019-08-05 23:21:58 +08:00
if (selectedOptions.length === 1) {
// If only one selected we use his name
2019-08-07 16:25:58 +08:00
drawTag({ label: selectedOptions[0].label, value: selectedOptions[0].value });
2019-08-05 23:21:58 +08:00
} else if (allOptionsSelected(selector, container)) {
// If all selected we use placeholder for all tags from select config
2019-08-07 16:25:58 +08:00
drawTag({ label: selector.data('select-multiple-all-selected'), value: 0 });
// Otherwise use placeholder from select config
2019-08-05 23:21:58 +08:00
} else if (selectedOptions.length > 1) {
2019-08-07 16:25:58 +08:00
drawTag({ label: `${selectedOptions.length} ${selector.data('select-multiple-name')}`, value: 0 });
2019-08-05 23:21:58 +08:00
}
} else {
// For normal tags we simpy draw each
2019-08-06 21:25:52 +08:00
$.each(selectedOptions, (ti, tag) => {
2019-08-07 16:25:58 +08:00
drawTag(tag);
});
2019-08-06 21:25:52 +08:00
}
// If we have alteast one tag, we need to remove placeholder from search field
if (selector.data('config').selectAppearance === 'simple') {
let selectedLabel = container.find('.tag-label');
2020-02-17 17:47:10 +08:00
container.find('.search-field').prop('placeholder',
selectedLabel.length && selectedLabel.text().trim() !== '' ? selectedLabel.text().trim() : selector.data('placeholder'));
} else {
searchFieldValue.attr('placeholder',
selectedOptions.length > 0 ? '' : (selector.data('placeholder') || ''));
}
2019-12-11 21:49:14 +08:00
searchFieldValue.attr('data-options-selected', selectedOptions.length);
2019-08-05 23:21:58 +08:00
// Add stretch class for visual improvments
if (!selector.data('combine-tags')) {
2019-08-07 16:25:58 +08:00
container.find('.ds-tags').addClass('stretch');
2019-08-06 21:25:52 +08:00
} else {
2019-08-07 16:25:58 +08:00
container.find('.ds-tags').removeClass('stretch');
2019-08-06 21:25:52 +08:00
}
// Update option container direction position
2019-08-07 16:25:58 +08:00
updateDropdownDirection(selector, container);
// Update options selection status
2019-08-07 16:25:58 +08:00
refreshDropdownSelection(selector, container);
// If dropdown active focus on search field
2019-09-16 22:20:16 +08:00
if (container.hasClass('open')) container.find('.search-field').focus();
2019-10-17 16:06:40 +08:00
// Trigger onSelect event
2019-10-18 17:31:13 +08:00
if (selector.data('config').onSelect && !config.skipChange && config.select && !config.skipSelect) {
2019-10-17 16:06:40 +08:00
selector.data('config').onSelect();
}
// Trigger onChange event
if (selector.data('config').onChange && !config.skipChange) {
2019-08-07 16:25:58 +08:00
selector.data('config').onChange();
}
2019-10-17 16:06:40 +08:00
// Trigger onUnSelect event
2019-10-18 17:31:13 +08:00
if (selector.data('config').onUnSelect && !config.skipChange && config.unselect && !config.skipUnselect) {
2019-10-17 16:06:40 +08:00
selector.data('config').onUnSelect(config.tagId);
}
// Close dropdown after select
if (selector.data('config').closeOnSelect && container.hasClass('open')) {
container.find('.input-field').click();
}
2019-08-06 21:25:52 +08:00
}
2019-08-07 16:25:58 +08:00
// Convert local data or ajax data to same format
function dataSource(selector, container) {
2019-08-06 21:25:52 +08:00
var result = [];
var groups;
var options;
var defaultParams;
var customParams;
var ajaxParams;
// If use AJAX we need to prepare correct format on backend
2019-08-07 16:25:58 +08:00
if (selector.data('ajax-url')) {
2019-08-26 21:49:33 +08:00
defaultParams = { query: container.find('.search-field').val() };
2019-08-07 16:25:58 +08:00
customParams = selector.data('config').ajaxParams;
ajaxParams = customParams ? customParams(defaultParams) : defaultParams;
2019-08-06 21:25:52 +08:00
$.get(selector.data('ajax-url'), ajaxParams, (data) => {
2020-02-17 17:47:10 +08:00
var optionsAjax = data.constructor === Array ? data : [];
2020-02-07 21:57:07 +08:00
if (selector.data('config').emptyOptionAjax) {
optionsAjax = [{
2020-02-17 17:47:10 +08:00
label: selector.data('placeholder') || '',
2020-02-07 21:57:07 +08:00
value: '',
group: null,
params: {}
2020-02-17 17:47:10 +08:00
}].concat(optionsAjax);
2020-02-07 21:57:07 +08:00
}
loadData(selector, container, optionsAjax);
2020-01-31 21:11:37 +08:00
PerfectSb().update_all();
2019-08-07 16:25:58 +08:00
});
// For local options we convert options element from select to correct array
2019-08-07 16:25:58 +08:00
} else if (selector.data('select-by-group')) {
groups = selector.find('optgroup');
$.each(groups, (gi, group) => {
var groupElement = { label: group.label, value: group.label, options: [] };
var groupOptions = filterOptions(selector, container, $(group).find('option'));
2019-08-07 16:25:58 +08:00
$.each(groupOptions, function(oi, option) {
groupElement.options.push({ label: option.innerHTML, value: option.value });
});
if (groupElement.options.length > 0) result.push(groupElement);
});
} else {
options = filterOptions(selector, container, selector.find('option'));
2019-08-07 16:25:58 +08:00
$.each(options, function(oi, option) {
result.push({
label: option.innerHTML,
value: option.value,
delimiter: option.dataset.delimiter,
params: JSON.parse(option.dataset.params || '{}')
});
2019-08-07 16:25:58 +08:00
});
2019-08-06 21:25:52 +08:00
}
2019-08-07 16:25:58 +08:00
return result;
2019-08-05 23:21:58 +08:00
}
function appendOptionToSelector(selector, value) {
2023-02-09 19:37:35 +08:00
$(selector).append(`<option
data-params=${JSON.stringify(value.params)}
value='${value.value}'
2023-02-09 19:37:35 +08:00
>${value.label}</option>`);
}
function removeOptionFromSelector(selector, id) {
$(selector).find(`option[value="${id}"]`).remove();
}
2019-08-07 16:25:58 +08:00
// ////////////////////
// Public functions ///
// ////////////////////
2019-08-02 21:57:41 +08:00
return {
2019-08-07 16:25:58 +08:00
// Dropdown initialization
2019-08-09 20:31:50 +08:00
init: function(selector, config) {
2019-08-07 16:25:58 +08:00
generateDropdown(selector, config);
2019-08-09 20:31:50 +08:00
return this;
},
// Clear button initialization
initClearButton: function(selector, clearButton) {
var container;
if ($(selector).length === 0) return false;
container = $(selector).next();
$(clearButton).click(() => {
updateCurrentData(container, []);
refreshDropdownSelection($(selector), container);
updateTags($(selector), container);
});
return this;
2019-08-02 21:57:41 +08:00
},
2019-08-07 16:25:58 +08:00
// Update dropdown position
2019-08-09 20:31:50 +08:00
updateDropdownDirection: function(selector) {
if ($(selector).length === 0) return false;
2019-11-27 21:04:59 +08:00
if (!$(selector).next().hasClass('open')) return false;
2019-08-07 16:25:58 +08:00
updateDropdownDirection($(selector), $(selector).next());
2019-08-09 20:31:50 +08:00
return this;
2019-08-06 21:25:52 +08:00
},
2019-08-07 16:25:58 +08:00
// Get only values
2019-08-09 20:31:50 +08:00
getValues: function(selector) {
var values;
if ($(selector).length === 0) return false;
values = $.map(getCurrentData($(selector).next()), (v) => {
2019-08-07 16:25:58 +08:00
return v.value;
});
if ($(selector).data('config').singleSelect) return values[0];
return values;
2019-08-07 16:25:58 +08:00
},
// Get selected labels
getLabels: function(selector) {
var labels;
if ($(selector).length === 0) return false;
labels = $.map(getCurrentData($(selector).next()), (v) => {
return v.label;
});
if ($(selector).data('config').singleSelect) return labels[0];
return labels;
},
2019-08-07 16:25:58 +08:00
// Get all data
2019-08-09 20:31:50 +08:00
getData: function(selector) {
if ($(selector).length === 0) return false;
2019-08-07 16:25:58 +08:00
return getCurrentData($(selector).next());
},
2019-08-09 20:31:50 +08:00
// Set data to selector
setData: function(selector, data) {
if ($(selector).length === 0) return false;
setData($(selector), data);
return this;
},
// Select values
selectValues: function(selector, values) {
var $selector = $(selector);
var option;
var valuesArray = [].concat(values);
var options = [];
if ($selector.length === 0) return false;
2019-08-09 20:31:50 +08:00
valuesArray.forEach(function(value) {
option = $selector.find(`option[value="${value}"]`);
option.attr('selected', true);
options.push(convertOptionToJson(option[0]));
});
setData($selector, options);
2019-08-09 20:31:50 +08:00
return this;
},
// Clear selector
clearData: function(selector) {
if ($(selector).length === 0) return false;
2019-10-18 17:31:13 +08:00
setData($(selector), []);
return this;
},
removeValue: function(selector, value, group = '', skip_event = false) {
var dropdownContainer;
if ($(selector).length === 0) return false;
dropdownContainer = $(selector).next();
deleteValue($(selector), dropdownContainer, value, null, skip_event);
return this;
},
addValue: function(selector, value, skip_event = false) {
var currentData;
if ($(selector).length === 0) return false;
currentData = getCurrentData($(selector).next());
currentData.push(value);
setData($(selector), currentData, skip_event);
if ($(selector).data('config').tagClass) {
appendOptionToSelector(selector, value);
}
2019-10-18 17:31:13 +08:00
2019-08-09 20:31:50 +08:00
return this;
},
2019-08-26 21:49:33 +08:00
// Enable selector
enableSelector: function(selector) {
if ($(selector).length === 0) return false;
disableEnableDropdown($(selector), $(selector).next(), false);
return this;
},
2019-08-09 20:31:50 +08:00
// Disable selector
2019-08-26 21:49:33 +08:00
disableSelector: function(selector) {
if ($(selector).length === 0) return false;
2019-08-26 21:49:33 +08:00
disableEnableDropdown($(selector), $(selector).next(), true);
2019-08-09 20:31:50 +08:00
return this;
2019-10-17 16:06:40 +08:00
},
// close dropdown
closeDropdown: function(selector) {
var dropdownContainer;
if ($(selector).length === 0) return false;
2019-10-18 17:31:13 +08:00
dropdownContainer = $(selector).next();
2019-10-17 16:06:40 +08:00
if (dropdownContainer.hasClass('open')) {
2019-10-18 17:31:13 +08:00
dropdownContainer.find('.input-field').click();
2019-10-17 16:06:40 +08:00
}
return this;
},
2019-10-18 17:31:13 +08:00
// get dropdown container
2019-10-17 16:06:40 +08:00
getContainer: function(selector) {
if ($(selector).length === 0) return false;
2019-10-18 17:31:13 +08:00
return $(selector).next();
},
// Run success animation on dropdown
highlightSuccess: function(selector) {
var container = $(selector).next();
if ($(selector).length === 0) return false;
container.addClass('success');
setTimeout(() => {
container.removeClass('success');
}, 1500);
return this;
},
// Run error animation on dropdown
highlightError: function(selector) {
var container = $(selector).next();
if ($(selector).length === 0) return false;
container.addClass('error');
setTimeout(() => {
container.removeClass('error');
}, 1500);
return this;
},
showError: function(selector, error) {
var container = $(selector).next();
if ($(selector).length === 0) return false;
container.addClass('error').attr('data-error-text', error);
return this;
},
hideError: function(selector) {
var container = $(selector).next();
if ($(selector).length === 0) return false;
container.removeClass('error');
return this;
},
showWarning: function(selector) {
var container = $(selector).next();
if ($(selector).length === 0) return false;
container.addClass('warning');
return this;
},
hideWarning: function(selector) {
var container = $(selector).next();
if ($(selector).length === 0) return false;
container.removeClass('warning');
return this;
2019-08-02 21:57:41 +08:00
}
};
2019-08-07 16:25:58 +08:00
}());