mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-29 03:16:22 +08:00
81a9a7225b
* Add initial implementation of inventory stock management [SCI-6402]
* Add stock column type (#3786)
* Add stock column type
* Create new stock column [SCI-6410]
* Small fixed for stock column [SCI-6410]
* Add stock column validations [SCI-6410]
Co-authored-by: Anton <anton@scinote.net>
* Add stock table renders [SCI-6412] (#3787)
Co-authored-by: Anton <anton@scinote.net>
* Implemented stock management modal [SCI-6417] (#3788)
* Implement inventory stock management modal [SCI-6417]
* Add generalized validation mechanic, some bugfixes [SCI-6417]
* Fix permission check and method name in RepositoryStockValue [SCI-6417]
* Add stock and consumption to assigned items section [SCI-6434] (#3793)
* Added UPDATE_STOCK_CONSUMPTION to MyModule permissons [SCI-6418] (#3781)
* Fix invalid return in reports jobs [SCI-6409] (#3777)
* Added UPDATE_STOCK_CONSUMPTION to MyModule permissons [SCI-6418]
* Add current/new amount section to stock [SCI-6416] (#3791)
Co-authored-by: Anton <anton@scinote.net>
* Task stock consumption modal[SCI-6444][SCI-6445] (#3798)
Add main logic for consumption on task [SCI-6444]
Co-authored-by: Anton <anton@scinote.net>
* Add stock management columns to snapshots [SCI-6448]
* Handle stock management in full table views [SCI-6440]
* Implement stock management activities [SCI-6452] (#3810)
* Implement stock management activities [SCI-6452]
* PR code fixes [SCI-6452]
* Implement import for repository stock values [SCI-6461] (#3818)
* Add permissions checking to assigned items view on tasks [SCI-6435] (#3801)
* Add front-end validation for comments field [SCI-6464] (#3829)
Co-authored-by: Anton <anton@scinote.net>
* Add stock consumption to pdf and docx [SCI-6460] (#3816)
Co-authored-by: Anton <anton@scinote.net>
* Add support of repository snapshots to assigned items section [SCI-6439] (#3828)
* Change subject of Repository (#3838)
* Fixed full view assigned items modal stock consumption (#3846)
* Implement repository snapshots displaying on assigned items full view modal [SCI-6442] (#3862)
* Implement low stock threshold column [SCI-6555] (#3907)
* Stock column fixes [SCI-6455] (#3878)
* Fix issues with stock column management [SCI-6455]
* Refactor stock column in datatables [SCI-6455]
* Fix tests [SCI-6486] (#3913)
* Fix tests [SCI-6486]
* Fix rspec tests [SCI-6486]
Co-authored-by: Anton <anton@scinote.net>
* Implement basic logic for Date/DateTime/Stock reminders [SCI-6554] (#3911)
* Implement basic logic for Date/DateTime/Stock reminders [SCI-6554]
* Implement bell icon [SCI-6500]
* Refactor reminder cells scope [SCI-6554]
* Add red dot for date/datetime reminder [SCI-6499] (#3924)
Co-authored-by: Anton <anton@scinote.net>
* Add reminders to stock modal [SCI-6557] (#3917)
Co-authored-by: Anton <anton@scinote.net>
* Add flyout for repository reminders [SCI-6501] (#3926)
Co-authored-by: Anton <anton@scinote.net>
* Fix moving experiment [SCI-6602] (#3927)
* Update Rails to 6.1.4.7 [SCI-6615] (#3928)
* Task inventory activity for assigned item consumption [SCI-6453] (#3830)
* Add task inventory activity [SCI-6453]
* Apply user's time zone in advanced filters for time data type [SCI-6585] (#3930)
* Bump version to 1.24.2
* Implement hidden repository cell reminders [SCI-6504] (#3933)
* Low stock flyout reminders [SCI-6502] (#3932)
* Display low stock flyout reminder [SCI-6502]
* Display low stock flyout reminder [SCI-6502]
* Adapt showing stock reminders for every bell icon click [SCI-6502]
* Correct hound error [SCI-6502]
* Fix event registration for fetching reminder data [SCI-6502]
* Fix event registration for fetching reminder data [SCI-6502]
* Fix hound [SCI-6502]
* Remove not needed line [SCI-6502]
* Add reminder template [SCI-6502]
* Add clearing messages [SCI-6502]
* Displaying low stock warning [SCI-6497] (#3912)
* Low stock warning [SCI-6497
* Fix some bugs [SCI-6497]
* Fix to long line [SCI-6497]
* Clean not needed information in renderes [SCI-6497]
* Icon bell reminder for an assigned item on the task [SCI-6506] (#3929)
* Add bell icon on assigned task [SCI-6506]
* Add flyout and improve quering [SCI-6506]
* Refactor repository toolbar [SCI-6545] (#3943)
Co-authored-by: Anton <anton@scinote.net>
* Add datetime reminders to columns modal [SCI-6556] (#3934)
Co-authored-by: Anton <anton@scinote.net>
* Hide reminders for archived repositories [SCI-6609] (#3941)
* Hide reminders for archived repositories [SCI-6609]
* Always pass repository in datatable helper [SCI-6609]
* Handle locked stock consumption state [SCI-6608] (#3942)
* Change env variable name for s3 bucket region [SCI-6603] (#3944)
* Add last_transition_error column to tasks [SCI-6610] (#3949)
* Add microtransactions to stock consumptions [SCI-6626] (#3948)
Co-authored-by: Anton <anton@scinote.net>
* Date remidner flyout [SCI-6503] (#3937)
* Implement date reminder flyout [SCI-6503]
* Show only days left for date reminder [SCI-6503]
* Pluralize day in reminders [SCI-6503]
* Create partials for different reminders [SCI-6503]
* Unify css for reminder [SCI-6503]
* Add micro interactions for stock modal [SCI-6625] (#3947)
Co-authored-by: Anton <anton@scinote.net>
* Hide options to add New Tags for users without permissions on Task level [SCI-6573] (#3945)
* Bump lodash-es from 4.17.15 to 4.17.21 (#3939)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.15 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.21)
---
updated-dependencies:
- dependency-name: lodash-es
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Implement clearing hidden repository reminders [SCI-6507] (#3936)
* Add stock filters [SCI-6547] (#3938)
Co-authored-by: Anton <anton@scinote.net>
* Fix assign view for task repository [SCI-6648] (#3954)
Co-authored-by: Anton <anton@scinote.net>
* Add micro-interactions to reminders flyout [SCI-6627] (#3951)
Co-authored-by: Anton <anton@scinote.net>
* Improve error reporting in task status transition [SCI-6611] (#3952)
* Add ordering to global search results [SCI-6639] (#3960)
* Fix multiple stock bugs [SCI-6651] (#3959)
Co-authored-by: Anton <anton@scinote.net>
* Fix large previews for csv files [SCI-6619] (#3961)
* Add alias to cell sort query, to prevent join conflicts [SCI-6649] (#3953)
* Add alias to cell sort query, to prevent join conflicts [SCI-6649]
* Added value_type to join condition to make use of DB index [SCI-6649]
* Update front end validation for columns managment [SCI-6657] (#3962)
Co-authored-by: Anton <anton@scinote.net>
* Print protocol smart annotation and table improvements [SCI-6566] (#3925)
* Improve table printing in the print menu [SCI-6566]
* Open smart annotation in new tab for print view [SCI-6566]
* Unify naming of same parameter [SCI-6566]
* Fix typo [SCI-6566]
* Fix hound errors [SCI-6566]
* Add stock management toggling [SCI-6653] (#3967)
Co-authored-by: Anton <anton@scinote.net>
* Fix caching of an experiment card dropdown [SCI-6606] (#3963)
* Add stock consumption permission to user roles [SCI-6665] (#3966)
* Show archive icon only on archived projects in the header [SCI-6617] (#3965)
* Fix assigned items fullview modal [SCI-6664] (#3968)
* Implement hide all repository reminders button [SCI-6505] (#3940)
* Viewer role does not see any tasks in archived project [SCI-6616] (#3958)
* Fix viewer role not see any tasks in archived project [SCI-6616]
* Fix rollback for add read archive permission migration [SCI-6616]
* Stock modal bugs [SCI-6667] (#3970)
Co-authored-by: Anton <anton@scinote.net>
* Always use Tika text extractor in server mode [SCI-6658] (#3957)
* Implement stock consumption via the API [SCI-6642] (#3964)
* Implement stock consumption via the API [SCI-6642]
* Remove unnecessary attribute from InventoryItemSerializer [SCI-6642]
* Amend permission check, add nested transaction support to consume_stock method [SCI-6642]
* Toolbar fixes [SCI-6670] (#3973)
Co-authored-by: Anton <anton@scinote.net>
* Make stock column work properly with different orders [SCI-6677] (#3974)
* Fix filters loading for stock filter [SCI-6687] (#3975)
Co-authored-by: Anton <anton@scinote.net>
* Hide reminders and disable stock consumption for archived items [SCI-6689] (#3978)
* Fix red dot for datetime columns [SCI-6684] (#3977)
Co-authored-by: Anton <anton@scinote.net>
* Fix saving of inventory date time reminders saving [SCI-6672] (#3976)
* Bump puma from 5.6.2 to 5.6.4 (#3972)
Bumps [puma](https://github.com/puma/puma) from 5.6.2 to 5.6.4.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v5.6.2...v5.6.4)
---
updated-dependencies:
- dependency-name: puma
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Don't load reminders if the stock feature is disabled [SCI-6673] (#3981)
* Don't load reminders if the stock feature is disabled [SCI-6673]
* Don't load reminders if the stock feature is disabled [SCI-6673]
* Unify stock management enabled check [6673]
* Update repository card view for stock [SCI-6685] (#3982)
Co-authored-by: Anton <anton@scinote.net>
* Disable stock managememnt for all tables in my module context [SCI-6692] (#3983)
* Implement failed snapshot pop-up on task screen [SCI-6612] (#3950)
* Implement failed snapshot pop-up on task screen [SCI-6612]
* Copy change [SCI-6612]
* Proper handling of snapshot transition error [SCI-6612]
* Move modal auto open logic to JS file [SCI-6612]
* Remove unnecessary .html_safe [SCI-6612]
* Fix assigning and consuming items [SCI-6686] (#3985)
* Repository toolbar CSS fixes [SCI-6676] (#3984)
Co-authored-by: Anton <anton@scinote.net>
* Add basic validation to reminder value to prevent integer overflow errors [SCI-6693] (#3986)
* Fix reminder dropdown on task screen [SCI-6678] (#3987)
Co-authored-by: Anton <anton@scinote.net>
* Fix inventory sorting with date time reminders [SCI-6683] (#3980)
* Fix css stock modal [SCI-6675] (#3989)
Co-authored-by: Anton <anton@scinote.net>
* Stock column management UX/UI fixes [SCI-6674] (#3979)
* Fix red dot for dates in repository [SCI-6696] (#3991)
Co-authored-by: Anton <anton@scinote.net>
* Added styling for failed snapshots in sidebar [SCI-6636] (#3990)
* Added styling for failed snapshots in sidebar [SCI-6636]
* Simplify markup for failed icon [SCI-6636]
* Turn off autocomplete for stock amount/reminder [SCI-6694] (#3988)
* Override error styling in stock column management [SCI-6674] (#3995)
* Fix repository full view tables without stock management [SCI-6703] (#3994)
* Add pagination to projects list [SCI-6655]
* Add reminder preset to date(time) reminders [SCI-6693] (#3996)
* Fix color on consumption link [SCI-6686] (#3992)
* Fix sorting by date time columns in inventories [SCI-6683] (#4002)
* Improve loading of the dashboard [SCI-6618] (#4001)
* Stock modal UX fixes [SCI-6714] (#3998)
Co-authored-by: Anton <anton@scinote.net>
* Fix css for stotck modal [SCI-6698] (#3999)
Co-authored-by: Anton <anton@scinote.net>
* API: add endpoint for updating of stock [SCI-6549] (#3955)
* Add test for Stock repository cell [SCI-6549]
* Create and update stock inventory cell [SCI-6549]
* Fix hound errors [SCI-6549]
* Fix ledger recording [SCI-6549]
* Fix api endpoint [SCI-6549]
* Fix hound [SCI-6549]
* Fix collapsing of assigned items tables [SCI-6705] (#4000)
* API: add endpoint for creating stock column [SCI-6550] (#3956)
* Add test for repositoty stock column api [SCI-6550]
* Create and update stock column [SCI-6550]
* Change stock unit [SCI-6550]
* Fix hound [SCI-6550]
* Fix on delete [SCI-6550]
* Fix houd [SCI-6550]
* Removed autofocus clear on open reminder flyout [SCI-6690] (#4003)
* Removed clear autofocus on open reminder flyout [SCI-6690]
* Fix hound [SCI-6690]
* Adding/changing stock consumption [SCI-6708] (#4005)
* Adding/changing stock consumption [SCI-6708]
* Fix hound [SCI-6708]
* Remove date red dots for snapshots [SCI-6715] (#4004)
Co-authored-by: Anton <anton@scinote.net>
* Add snapshot error message below task status [SCI-6614] (#4009)
* Add repository snapshot error modal [SCI-6613] (#3993)
* Bump nokogiri from 1.13.3 to 1.13.4 (#4008)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.3 to 1.13.4.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/v1.13.4/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.3...v1.13.4)
---
updated-dependencies:
- dependency-name: nokogiri
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Add pagination for experiments [SCI-6656]
* Fix sorting by time and time range columns [SCI-6683] (#4011)
* Bump moment from 2.24.0 to 2.29.2 (#4006)
Bumps [moment](https://github.com/moment/moment) from 2.24.0 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.24.0...2.29.2)
---
updated-dependencies:
- dependency-name: moment
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Align items in assigned item section [SCI-6707] (#4007)
* Add locking and unique index to repository row assigning [SCI-6591] (#3921)
* tmp (#3935)
* Revert "tmp (#3935)" (#4014)
This reverts commit 043086d55f
.
* Move cursor to end of value when focusing consumption amount [SCI-6727] (#4013)
* Fix removing stock reminder threshold [SCI-6724] (#4010)
* Stock small ux fixes [SCI-6714] (#4015)
Co-authored-by: Anton <anton@scinote.net>
* Add aibility to delete stock column [SCI-6723] (#4017)
Co-authored-by: Anton <anton@scinote.net>
* Fix red dot for date reminders [SCI-6696] (#4016)
Co-authored-by: Anton <anton@scinote.net>
* Change the way setting stock works [SCI-6730] (#4018)
* Fix error modal opening on successful snapshot [SCI-6726] (#4019)
* Refactor and fix display of stock warnings and stock consumption [SCI-6734] (#4021)
* Refactor and fix how stock consumption is displayed [SCI-6734]
* Refactor and fix display of stock warnings [SCI-6734]
* Fix stock consumption in reports [SCI-6735]
* Fix markup
* Focus authenticator code field [SCI-6716] (#4022)
* Hide reminders settings and red dots if stock is disabled [SCI-6673] (#4024)
* Fix decimals in stock consumption modal [SCI-6732]
* Add edit title for stock consumption modal [SCI-6731]
* Highlight negative stock in modal with red [SCI-6729]
* Stock management test [SCI-6420] (#3946)
* Initial test for stock management
* Repository stock values adding [does not work] SCI-6420
* Fix typos SCI-6420
* Fix test [SCI-6420]
* Test changes [SCI-6402]
* Remove locking test [SCI-6420]
* Remove lock [SCI-6420]
* Remove serializer [SCI-6420]
* Fix stock test and add ledger creation on consume [SCI-6420]
* Fix inviting users to team [SCI-6725] (#4026)
* Improve stock ledger records creation [SCI-6419]
* Fix stock modal add/remove value preset value [SCI-6740]
* API GET repository stock column output fix [SCI-6550] (#4030)
* Add include stock unit items to Stock column get [SCI-6550]
* Fix stock column test [SCI-6550]
* Fix hound [SCI-6550]
* Move list-type column includes to param, unify naming [SCI-6738] (#4036)
* Add default includes for inventory columns with lists [SCI-6738]
* Move list-type column includes to param, unify naming [SCI-6738]
* Fix snapshot consumption display [SCI-6734] (#4035)
* Fix decimal render function [SCI-6742] (#4037)
Co-authored-by: Anton <anton@scinote.net>
* Add comment to repository stock activities [SCI-6746]
* Fix default stock column default units selection [SCI-6744]
* Add negative validation for stock [SCI-6743]
* Highlight negative value in consumption modal [SCI-6741]
* Fix snapshot creation with stock consumption [SCI-6762]
* Update stock value without reloading the table [SCI-6745] (#4041)
* Allow tag creation on task level if you have permission [SCI-6573] (#4031)
* Enable team normal user to edit repository files [SCI-6765] (#4049)
* Add negative validation for treshold [SCI-6743]
* Copy fix [SCI-6762] (#4048)
* Add bell icon for negative stock [SCI-6770]
* Fix formatting of stock consumption on tasks [SCI-6737] (#4050)
* Fix duplicated inventories on tasks for export all [SCI-6776]
* Stock / stock consumption display fixes [SCI-6771] (#4054)
* Fix blank stock consumption representation in reports [SCI-6769]
* Upgrade Rails to 6.1.5.1
* Show last page message only after second page [SCI-6761]
* Small CSS fixes for repository [SCI-6767]
* Added validations for date(time) column reminder [SCI-6775] (#4063)
* Upgrade Ruby to 2.7.6
* Open repository on print view [SCI-6566] (#4055)
* Fix saving of new date time columns with reminders [SCI-6774] (#4068)
* Improve input field for adding new team [SCI-6155]
* Allow deleteion of stock columns when stock management is disabled [SCI-6800]
* Update default label template [SCI-6763] (#4064)
* Stock editing and reminder fixes [SCI-6803] (#4078)
* Fix stock editing after adding new value [SCI-6803]
* Don't load reminders for snapshots [SCI-6803]
* Improve loading time of canvas view/edit [SCI-6797]
* Fix stock consumption rounding [SCI-6807]
* Fix displaying of stock consumption units [SCI-6795]
* Fix task dropdown actions [SCI-6817]
* Fix archived tasks view [SCI-6822]
* Fix display of repository snapshot with stock consumption [SCI-6824]
* Fix duplicated counters on task card [SCI-6825]
* Bump version to 1.25.0
Co-authored-by: Oleksii Kriuchykhin <okriuchykhin@biosistemika.com>
Co-authored-by: aignatov-bio <47317017+aignatov-bio@users.noreply.github.com>
Co-authored-by: Anton <anton@scinote.net>
Co-authored-by: ajugo <andrej.jugovic7@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1068 lines
38 KiB
JavaScript
1068 lines
38 KiB
JavaScript
/* global PerfectScrollbar activePSB PerfectSb I18n */
|
|
/* eslint-disable no-unused-vars, no-use-before-define */
|
|
|
|
/*
|
|
|
|
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
|
|
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
|
|
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
|
|
emptyOptionAjax: boolean, // Add empty option for ajax request
|
|
labelHTML: bolean, // render as HTMLelement or text
|
|
}
|
|
|
|
|
|
*/
|
|
|
|
var dropdownSelector = (function() {
|
|
// /////////////////////
|
|
// Support functions //
|
|
// ////////////////////
|
|
const MAX_DROPDOWN_HEIGHT = 320;
|
|
|
|
// Change direction of dropdown depends of container position
|
|
function updateDropdownDirection(selector, container) {
|
|
var windowHeight = $(window).height();
|
|
var containerPosition = container[0].getBoundingClientRect().top;
|
|
var containerPositionLeft = container[0].getBoundingClientRect().left;
|
|
var containerHeight = container[0].getBoundingClientRect().height;
|
|
var containerWidth = container[0].getBoundingClientRect().width;
|
|
var bottomSpace;
|
|
var modalContainer = container.closest('.modal-dialog');
|
|
var modalContainerBottom = 0;
|
|
var maxHeight = 0;
|
|
const bottomTreshold = 280;
|
|
|
|
if (modalContainer.length && windowHeight !== modalContainer.height()) {
|
|
let modalClientRect = modalContainer[0].getBoundingClientRect();
|
|
windowHeight = modalContainer.height() + modalClientRect.top;
|
|
containerPositionLeft -= modalClientRect.left;
|
|
modalContainerBottom = windowHeight + modalClientRect.bottom;
|
|
maxHeight += modalContainerBottom;
|
|
}
|
|
bottomSpace = windowHeight - containerPosition - containerHeight;
|
|
|
|
if ((modalContainerBottom + bottomSpace) < bottomTreshold) {
|
|
container.addClass('inverse');
|
|
maxHeight = Math.min(containerPosition - 122 + maxHeight, MAX_DROPDOWN_HEIGHT);
|
|
container.find('.dropdown-container').css('max-height', `${maxHeight}px`)
|
|
.css('margin-bottom', `${(containerPosition * -1)}px`)
|
|
.css('left', `${containerPositionLeft}px`)
|
|
.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`)
|
|
.css('left', `${containerPositionLeft}px`)
|
|
.css('margin-top', `${(bottomSpace * -1)}px`);
|
|
}
|
|
}
|
|
|
|
// Get data in JSON from field
|
|
function getCurrentData(container) {
|
|
if (!container.find('.data-field').val()) {
|
|
return '';
|
|
}
|
|
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();
|
|
}
|
|
|
|
// Search filter for non-ajax data
|
|
function filterOptions(selector, container, options) {
|
|
var customFilter = selector.data('config').localFilter;
|
|
var searchQuery = container.find('.search-field').val();
|
|
|
|
var data = customFilter ? customFilter(options) : options;
|
|
if (searchQuery.length === 0) return data;
|
|
return $.grep(data, (n) => {
|
|
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="${selectedOption.value}"][data-group="${selectedOption.group || ''}"]`)
|
|
.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);
|
|
}
|
|
|
|
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 });
|
|
searchFieldValue.attr('placeholder', selector.data('disable-placeholder') || '');
|
|
container.addClass('disabled').removeClass('open')
|
|
.find('.search-field').val('')
|
|
.prop('disabled', true);
|
|
} else {
|
|
container.removeClass('disabled')
|
|
.find('.search-field').prop('disabled', false);
|
|
updateTags(selector, container, { skipChange: true });
|
|
}
|
|
}
|
|
|
|
// Read option to JSON
|
|
function convertOptionToJson(option) {
|
|
if (option === undefined) {
|
|
return { label: '', value: '', params: {} };
|
|
}
|
|
return {
|
|
label: option.innerHTML,
|
|
value: option.value,
|
|
group: option.dataset.group || '',
|
|
params: JSON.parse(option.dataset.params || '{}')
|
|
};
|
|
}
|
|
|
|
function noOptionsForSelect(selector) {
|
|
return !$(selector).data('ajax-url') && $(selector).find('.dropdown-option').length == 0;
|
|
}
|
|
|
|
// Ajax intial values, we will use default options //
|
|
function ajaxInitialValues(selector, container) {
|
|
var intialData = [];
|
|
$(selector).find('option').each((i, option) => {
|
|
intialData.push(convertOptionToJson(option));
|
|
});
|
|
updateCurrentData(container, intialData);
|
|
updateTags(selector, container, { skipChange: true });
|
|
}
|
|
|
|
// Add selected option to value
|
|
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;
|
|
}
|
|
|
|
// Prepare custom dropdown icon
|
|
function prepareCustomDropdownIcon(selector, config) {
|
|
if (config.inputTagMode && noOptionsForSelect(selector)) {
|
|
return '';
|
|
}
|
|
if (config.customDropdownIcon) {
|
|
return config.customDropdownIcon();
|
|
}
|
|
return '<i class="fas fa-caret-down right-icon"></i><i class="fas fa-search right-icon simple-dropdown"></i>';
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
if (selectedOption.prev('.dropdown-option').length) {
|
|
selectedOption.removeClass('highlight').prev().addClass('highlight');
|
|
}
|
|
if (selectedOption.prev('.delimiter').length) {
|
|
selectedOption.removeClass('highlight').prev().prev().addClass('highlight');
|
|
}
|
|
} else if (pressedKey === 40) { // arrow down
|
|
if (selectedOption.next('.dropdown-option').length) {
|
|
selectedOption.removeClass('highlight').next().addClass('highlight');
|
|
}
|
|
if (selectedOption.next('.delimiter').length) {
|
|
selectedOption.removeClass('highlight').next().next().addClass('highlight');
|
|
}
|
|
} else if (pressedKey === 8 && e.target.value === '') { // backspace
|
|
deleteTag(selector, container, container.find('.ds-tags .fa-times').last());
|
|
}
|
|
});
|
|
}
|
|
|
|
// //////////////////////
|
|
// Private functions ///
|
|
// /////////////////////
|
|
|
|
// Initialization of dropdown
|
|
function generateDropdown(selector, config = {}) {
|
|
var selectElement = $(selector);
|
|
var optionContainer;
|
|
var perfectScroll;
|
|
var dropdownContainer;
|
|
var toggleElement;
|
|
|
|
if (selectElement.length === 0) return;
|
|
|
|
// Check if element exist or already initialized
|
|
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();
|
|
|
|
// Save config info to select element
|
|
selectElement.data('config', config);
|
|
|
|
// Draw main elements
|
|
$(`
|
|
<div class="dropdown-container"></div>
|
|
<div class="input-field">
|
|
<input type="text" class="search-field" data-options-selected=0 placeholder="${selectElement.data('placeholder') || ''}"></input>
|
|
${prepareCustomDropdownIcon(selector, config)}
|
|
</div>
|
|
<input type="hidden" class="data-field" value="[]">
|
|
|
|
`).appendTo(dropdownContainer);
|
|
|
|
// 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
|
|
if (selectElement.data('select-all-button')) {
|
|
$(`<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"
|
|
if (allOptionsSelected(selectElement, dropdownContainer) || selectElement.data('ajax-url')) {
|
|
dropdownContainer.find('.dropdown-group, .dropdown-option').removeClass('select');
|
|
} else {
|
|
dropdownContainer.find('.dropdown-group, .dropdown-option').addClass('select');
|
|
}
|
|
saveData(selectElement, dropdownContainer);
|
|
});
|
|
}
|
|
|
|
|
|
if (selectElement.data('ajax-url') || config.inputTagMode) {
|
|
// If we use AJAX dropdown or tags input, options become initial values
|
|
ajaxInitialValues(selectElement, dropdownContainer);
|
|
}
|
|
|
|
|
|
// When we start typing it will dynamically update data
|
|
dropdownContainer.find('.search-field')
|
|
.keyup((e) => {
|
|
if (e.keyCode === 38
|
|
|| e.keyCode === 40
|
|
|| (config.selectKeys || []).includes(e.keyCode)) {
|
|
return;
|
|
}
|
|
e.stopPropagation();
|
|
loadData(selectElement, dropdownContainer);
|
|
})
|
|
.keypress((e) => {
|
|
if ((config.selectKeys || [13]).includes(e.keyCode)) {
|
|
if (config.inputTagMode) {
|
|
addNewTag(selectElement, dropdownContainer);
|
|
} 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();
|
|
}
|
|
});
|
|
|
|
// Initialize scroll bar inside options container
|
|
perfectScroll = new PerfectScrollbar(dropdownContainer.find('.dropdown-container')[0]);
|
|
activePSB.push(perfectScroll);
|
|
|
|
// Select options container
|
|
optionContainer = dropdownContainer.find('.dropdown-container');
|
|
|
|
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
|
|
dropdownContainer.find('.input-field').click(() => {
|
|
// Now we can have only one dropdown opened at same time
|
|
$('.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
|
|
optionContainer.scrollTo(0);
|
|
|
|
// Now open/close option container
|
|
dropdownContainer.toggleClass('open');
|
|
if (dropdownContainer.hasClass('open')) {
|
|
// on Open we load new data
|
|
loadData(selectElement, dropdownContainer);
|
|
updateDropdownDirection(selectElement, dropdownContainer);
|
|
|
|
dropdownContainer.find('.search-field').focus();
|
|
|
|
// onOpen event
|
|
if (config.onOpen) {
|
|
config.onOpen();
|
|
}
|
|
} else {
|
|
// on Close we blur search field
|
|
dropdownContainer.find('.search-field').blur().val('');
|
|
|
|
// onClose event
|
|
if (config.onClose) {
|
|
config.onClose();
|
|
}
|
|
}
|
|
});
|
|
|
|
// When user will resize browser we must check dropdown position
|
|
$(window).resize(() => { updateDropdownDirection(selectElement, dropdownContainer); });
|
|
$(window).scroll(() => { updateDropdownDirection(selectElement, dropdownContainer); });
|
|
|
|
// When user will click away, we must close dropdown
|
|
$(window).click(() => {
|
|
if (dropdownContainer.hasClass('open')) {
|
|
dropdownContainer.find('.search-field').val('');
|
|
}
|
|
if (dropdownContainer.hasClass('open') && config.onClose) {
|
|
config.onClose();
|
|
}
|
|
dropdownContainer.removeClass('open active');
|
|
});
|
|
|
|
// Prevent closing dropdown if we click inside
|
|
dropdownContainer.click((e) => { e.stopPropagation(); });
|
|
|
|
// Hide original select element
|
|
selectElement.css('display', 'none');
|
|
|
|
|
|
// Disable dropdown by default
|
|
if (selectElement.data('disable-on-load')) disableEnableDropdown(selectElement, dropdownContainer, true);
|
|
|
|
// EnableView mode
|
|
if (selectElement.data('view-mode')) {
|
|
enableViewMode(selectElement, dropdownContainer);
|
|
}
|
|
|
|
// Select default value
|
|
if (!selectElement.data('ajax-url')) {
|
|
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);
|
|
|
|
// In some case dropdown position not correctly calculated
|
|
updateDropdownDirection(selectElement, dropdownContainer);
|
|
}
|
|
|
|
// Load data to dropdown list
|
|
function loadData(selector, container, ajaxData = null) {
|
|
var data;
|
|
var containerDropdown = container.find('.dropdown-container');
|
|
// We need to remeber previos option container size before update
|
|
containerDropdown.css('height', `${containerDropdown.height()}px`);
|
|
if (ajaxData) {
|
|
// For ajax we simpy use data from request
|
|
data = ajaxData;
|
|
} else {
|
|
// For local from select options
|
|
data = dataSource(selector, container);
|
|
}
|
|
|
|
// Draw option object
|
|
function drawOption(selector2, option, group = null) {
|
|
// Check additional params from config
|
|
var params = option.params || {};
|
|
var customLabel = selector2.data('config').optionLabel;
|
|
var customClass = params.optionClass || selector2.data('config').optionClass || '';
|
|
var customStyle = selector2.data('config').optionStyle;
|
|
return $(`
|
|
<div class="dropdown-option ${customClass}" style="${customStyle ? customStyle(option) : ''}"
|
|
title="${(option.params && option.params.tooltip) || ''}"
|
|
data-params='${JSON.stringify(option.params || {})}'
|
|
data-label="${option.label}"
|
|
data-group="${group ? group.value : ''}"
|
|
data-value="${option.value}">
|
|
${customLabel ? customLabel(option) : option.label}
|
|
</div>"
|
|
`);
|
|
}
|
|
|
|
// Draw delimiter object
|
|
function drawDelimiter() {
|
|
return $('<div class="delimiter"></div>');
|
|
}
|
|
|
|
// Draw group object
|
|
function drawGroup(group) {
|
|
return $(`
|
|
<div class="dropdown-group">
|
|
<div class="group-name checkbox-icon">${group.label}</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
// Click action for option object
|
|
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();
|
|
}
|
|
$(this).toggleClass('select');
|
|
saveData(selector, $container);
|
|
}
|
|
|
|
// Remove placeholder from option container
|
|
container.find('.dropdown-group, .dropdown-option, .empty-dropdown, .dropdown-hint, .delimiter').remove();
|
|
if (!data) return;
|
|
|
|
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
|
|
if (selector.data('select-by-group')) {
|
|
$.each(data, function(gi, group) {
|
|
// First we create our group
|
|
var groupElement = drawGroup(group);
|
|
// Now add options to this group
|
|
$.each(group.options, function(oi, option) {
|
|
var optionElement = drawOption(selector, option, group);
|
|
optionElement.click(clickOption);
|
|
optionElement.appendTo(groupElement);
|
|
});
|
|
|
|
// Now for each group we add action to select all options
|
|
groupElement.find('.group-name').click(function() {
|
|
var groupContainer = $(this).parent();
|
|
|
|
// Disable group select to single select
|
|
if (selector.data('config').singleSelect) return;
|
|
|
|
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
|
|
groupElement.appendTo(container.find('.dropdown-container'));
|
|
});
|
|
} else {
|
|
// For simple options we only draw them
|
|
$.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);
|
|
optionElement.click(clickOption);
|
|
optionElement.appendTo(container.find('.dropdown-container'));
|
|
});
|
|
}
|
|
} else {
|
|
// If we data empty, draw placeholder
|
|
$(`<div class="empty-dropdown">${I18n.t('dropdown_selector.nothing_found')}</div>`).appendTo(container.find('.dropdown-container'));
|
|
}
|
|
|
|
// Update scrollbar
|
|
PerfectSb().update_all();
|
|
|
|
// Check position of option dropdown
|
|
refreshDropdownSelection(selector, container);
|
|
|
|
// Unfreeze option container height
|
|
containerDropdown.css('height', 'auto');
|
|
}
|
|
|
|
// Save data to local field
|
|
function saveData(selector, container) {
|
|
// Check what we have now selected
|
|
var selectArray = getCurrentData(container);
|
|
|
|
// Search option by value and group
|
|
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('');
|
|
|
|
// Now we check all options in dropdown for selection and add them to array
|
|
$.each(container.find('.dropdown-container .dropdown-option'), function(oi, option) {
|
|
var alreadySelected;
|
|
var toDelete;
|
|
var newOption;
|
|
if ($(option).hasClass('select')) {
|
|
alreadySelected = findOption(selectArray, option);
|
|
|
|
// If it is new option we add it
|
|
if (alreadySelected === -1) {
|
|
newOption = {
|
|
label: option.dataset.label,
|
|
value: option.dataset.value,
|
|
group: option.dataset.group,
|
|
params: JSON.parse(option.dataset.params)
|
|
};
|
|
selectArray.push(newOption);
|
|
}
|
|
} else {
|
|
// If we deselect option we remove it
|
|
toDelete = findOption(selectArray, option);
|
|
if (toDelete >= 0) selectArray.splice(toDelete, 1);
|
|
}
|
|
// This complex required to save order of tags
|
|
});
|
|
// Now we save new data
|
|
updateCurrentData(container, selectArray);
|
|
// Redraw tags
|
|
updateTags(selector, container, { select: true });
|
|
}
|
|
|
|
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'));
|
|
}
|
|
}, 350);
|
|
}
|
|
|
|
// Refresh tags in input field
|
|
function updateTags(selector, container, config = {}) {
|
|
var selectedOptions = getCurrentData(container);
|
|
var searchFieldValue = container.find('.search-field');
|
|
|
|
// Draw tag and assign event
|
|
function drawTag(data) {
|
|
// Check for custom options
|
|
var customLabel = selector.data('config').tagLabel;
|
|
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';
|
|
var label = customLabel ? customLabel(data) : data.label;
|
|
// Add new tag before search field
|
|
var tag = $(`<div class="${tagAppearance} ${customClass}" style="${customStyle ? customStyle(data) : ''}" >
|
|
<div class="tag-label"
|
|
title="${(data.params && data.params.tooltip) || $('<span>' + label + '</span>').text().trim()}"
|
|
data-ds-tag-group="${data.group}"
|
|
data-ds-tag-id="${data.value}">
|
|
</div>
|
|
<i class="fas fa-times ${selector.data('config').singleSelect ? 'hidden' : ''}"></i>
|
|
</div>`).insertBefore(container.find('.input-field .search-field'));
|
|
|
|
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
|
|
tag.find('.fa-times').click(function(e) {
|
|
e.stopPropagation();
|
|
deleteTag(selector, container, $(this));
|
|
});
|
|
}
|
|
|
|
// Clear all tags
|
|
container.find('.ds-tags, .ds-simple').remove();
|
|
if (selector.data('combine-tags')) {
|
|
// If we use combine-tags options we draw only one tag
|
|
if (selectedOptions.length === 1) {
|
|
// If only one selected we use his name
|
|
drawTag({ label: selectedOptions[0].label, value: selectedOptions[0].value });
|
|
} else if (allOptionsSelected(selector, container)) {
|
|
// If all selected we use placeholder for all tags from select config
|
|
drawTag({ label: selector.data('select-multiple-all-selected'), value: 0 });
|
|
// Otherwise use placeholder from select config
|
|
} else if (selectedOptions.length > 1) {
|
|
drawTag({ label: `${selectedOptions.length} ${selector.data('select-multiple-name')}`, value: 0 });
|
|
}
|
|
} else {
|
|
// For normal tags we simpy draw each
|
|
$.each(selectedOptions, (ti, tag) => {
|
|
drawTag(tag);
|
|
});
|
|
}
|
|
|
|
// 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');
|
|
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') || ''));
|
|
}
|
|
|
|
searchFieldValue.attr('data-options-selected', selectedOptions.length);
|
|
|
|
// Add stretch class for visual improvments
|
|
if (!selector.data('combine-tags')) {
|
|
container.find('.ds-tags').addClass('stretch');
|
|
} else {
|
|
container.find('.ds-tags').removeClass('stretch');
|
|
}
|
|
|
|
// Update option container direction position
|
|
updateDropdownDirection(selector, container);
|
|
// Update options selection status
|
|
refreshDropdownSelection(selector, container);
|
|
|
|
// If dropdown active focus on search field
|
|
if (container.hasClass('open')) container.find('.search-field').focus();
|
|
|
|
// Trigger onSelect event
|
|
if (selector.data('config').onSelect && !config.skipChange && config.select && !config.skipSelect) {
|
|
selector.data('config').onSelect();
|
|
}
|
|
|
|
// Trigger onChange event
|
|
if (selector.data('config').onChange && !config.skipChange) {
|
|
selector.data('config').onChange();
|
|
}
|
|
|
|
// Trigger onUnSelect event
|
|
if (selector.data('config').onUnSelect && !config.skipChange && config.unselect && !config.skipUnselect) {
|
|
selector.data('config').onUnSelect(config.tagId);
|
|
}
|
|
|
|
// Close dropdown after select
|
|
if (selector.data('config').closeOnSelect && container.hasClass('open')) {
|
|
container.find('.input-field').click();
|
|
}
|
|
}
|
|
|
|
// Convert local data or ajax data to same format
|
|
function dataSource(selector, container) {
|
|
var result = [];
|
|
var groups;
|
|
var options;
|
|
var defaultParams;
|
|
var customParams;
|
|
var ajaxParams;
|
|
// If use AJAX we need to prepare correct format on backend
|
|
if (selector.data('ajax-url')) {
|
|
defaultParams = { query: container.find('.search-field').val() };
|
|
customParams = selector.data('config').ajaxParams;
|
|
ajaxParams = customParams ? customParams(defaultParams) : defaultParams;
|
|
|
|
$.get(selector.data('ajax-url'), ajaxParams, (data) => {
|
|
var optionsAjax = data.constructor === Array ? data : [];
|
|
if (selector.data('config').emptyOptionAjax) {
|
|
optionsAjax = [{
|
|
label: selector.data('placeholder') || '',
|
|
value: '',
|
|
group: null,
|
|
params: {}
|
|
}].concat(optionsAjax);
|
|
}
|
|
loadData(selector, container, optionsAjax);
|
|
PerfectSb().update_all();
|
|
});
|
|
// For local options we convert options element from select to correct array
|
|
} 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'));
|
|
$.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'));
|
|
$.each(options, function(oi, option) {
|
|
result.push({
|
|
label: option.innerHTML,
|
|
value: option.value,
|
|
delimiter: option.dataset.delimiter,
|
|
params: JSON.parse(option.dataset.params || '{}')
|
|
});
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ////////////////////
|
|
// Public functions ///
|
|
// ////////////////////
|
|
|
|
return {
|
|
// Dropdown initialization
|
|
init: function(selector, config) {
|
|
generateDropdown(selector, config);
|
|
|
|
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;
|
|
},
|
|
// Update dropdown position
|
|
updateDropdownDirection: function(selector) {
|
|
if ($(selector).length === 0) return false;
|
|
|
|
if (!$(selector).next().hasClass('open')) return false;
|
|
|
|
updateDropdownDirection($(selector), $(selector).next());
|
|
|
|
return this;
|
|
},
|
|
// Get only values
|
|
getValues: function(selector) {
|
|
var values;
|
|
if ($(selector).length === 0) return false;
|
|
values = $.map(getCurrentData($(selector).next()), (v) => {
|
|
return v.value;
|
|
});
|
|
if ($(selector).data('config').singleSelect) return values[0];
|
|
|
|
return values;
|
|
},
|
|
// 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;
|
|
},
|
|
// Get all data
|
|
getData: function(selector) {
|
|
if ($(selector).length === 0) return false;
|
|
|
|
return getCurrentData($(selector).next());
|
|
},
|
|
|
|
// 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;
|
|
|
|
valuesArray.forEach(function(value) {
|
|
option = $selector.find(`option[value="${value}"]`);
|
|
option.attr('selected', true);
|
|
options.push(convertOptionToJson(option[0]));
|
|
});
|
|
setData($selector, options);
|
|
return this;
|
|
},
|
|
|
|
// Clear selector
|
|
clearData: function(selector) {
|
|
if ($(selector).length === 0) return false;
|
|
|
|
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);
|
|
|
|
return this;
|
|
},
|
|
|
|
// Enable selector
|
|
enableSelector: function(selector) {
|
|
if ($(selector).length === 0) return false;
|
|
|
|
disableEnableDropdown($(selector), $(selector).next(), false);
|
|
|
|
return this;
|
|
},
|
|
|
|
// Disable selector
|
|
disableSelector: function(selector) {
|
|
if ($(selector).length === 0) return false;
|
|
|
|
disableEnableDropdown($(selector), $(selector).next(), true);
|
|
|
|
return this;
|
|
},
|
|
|
|
// close dropdown
|
|
closeDropdown: function(selector) {
|
|
var dropdownContainer;
|
|
|
|
if ($(selector).length === 0) return false;
|
|
dropdownContainer = $(selector).next();
|
|
if (dropdownContainer.hasClass('open')) {
|
|
dropdownContainer.find('.input-field').click();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
// get dropdown container
|
|
getContainer: function(selector) {
|
|
if ($(selector).length === 0) return false;
|
|
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;
|
|
}
|
|
};
|
|
}());
|