bootstrap-select-dropdown
Version:
A jQuery plugin for Bootstrap 4 that converts <select> and <select multiselect> elements to dropdowns. Uses fuse.js for fuzzy search and Bootstrap's dropdown plugin.
1,345 lines (1,220 loc) • 37 kB
JavaScript
import $ from 'jquery'
import Util from 'bootstrap/js/src/util'
import Fuse from 'fuse.js'
let SelectDropdownIndex = 1
/**
* --------------------------------------------------------------------------
* Bootstrap Select Dropdown
* --------------------------------------------------------------------------
*/
const SelectDropdown = (($) => {
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'selectDropdown'
const VERSION = '[AIV]{version}[/AIV]'
const DATA_KEY = 'bs.selectDropdown'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const JQUERY_NO_CONFLICT = $.fn[NAME]
const KEYUP_TIMEOUT = 300
const ENTER_KEYCODE = 13
const ESCAPE_KEYCODE = 27
const ARROW_UP_KEYCODE = 38
const ARROW_DOWN_KEYCODE = 40
const Default = {
// Profile
profile: "default",
// Behaviour
hideSelect: true,
search: true,
maxHeight: '300px',
keyboard: true,
badges: true, // Multiselect only
badgesDismissable: true, // Multiselect only
maxListLength: 0, // Multiselect only
// Text
textNoneSelected: "Select",
textMultipleSelected: "%count_selected% selected",
textNoResults: "No results",
// Controls
deselectAll: true, // Multiselect only
selectAll: true, // Multiselect only
showSelected: true, // Multiselect only
// Buttons
selectButtons : false,
classBtnDeselectAll : "btn btn-outline-secondary", // Multiselect only
classBtnSelectAll : "btn btn-outline-secondary", // Multiselect only
// HTML
htmlClear: "Clear search",
htmlDeselectAll: "Deselect all", // Multiselect only
htmlSelectAll: "Select all", // Multiselect only
htmlBadgeRemove: "[X]", // Badges only
// Classes
classBtnSelect : "btn btn-primary",
classBadge: "badge badge-dark mr-1 mb-1",
classBadgeLink: "text-white",
classBadgeContainer : "mt-2 mb-3",
// Callbacks
loaded : function(){}
}
const DefaultType = {
maxListLength : 'number',
hideSelect : 'boolean',
search : 'boolean',
maxHeight : 'string',
keyboard : 'boolean',
badges : 'boolean',
badgesDismissable : 'boolean',
textNoneSelected : 'string',
textMultipleSelected : 'string',
textNoResults : 'string',
deselectAll : 'boolean',
selectAll : 'boolean',
selectButtons : 'boolean',
classBtnDeselectAll : 'string',
classBtnSelectAll : 'string',
htmlClear : 'string',
htmlDeselectAll : 'string',
htmlSelectAll : 'string',
htmlBadgeRemove : 'string',
classBtnSelect : 'string',
classBadge : 'string',
classBadgeLink : 'string',
classBadgeContainer : 'string',
loaded : 'function'
}
const Event = {
CLICK : `click${EVENT_KEY}`,
KEYUP : `keyup${EVENT_KEY}`,
KEYDOWN : `keydown${EVENT_KEY}`,
FOCUS : `focus${EVENT_KEY}`,
BLUR : `blur${EVENT_KEY}`,
FOCUSIN : `focusin${EVENT_KEY}`,
LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`
}
const ClassName = {
ACTIVE : 'active',
BG_TRANSPARENT : 'bg-transparent',
DISABLED : 'disabled',
DROPDOWN : 'dropdown',
MENU : 'dropdown-menu',
ITEM : 'dropdown-item',
BTN_GROUP : 'btn-group',
INPUT_GROUP : 'input-group',
INPUT_GROUP_APPEND : 'input-group-append',
HOVER : 'hover',
HOVER_BG : 'bg-light',
TEXT_MUTED : 'text-muted',
ALIGNMENT_RIGHT : 'dropdown-menu-right',
}
const Selector = {
DATA_ROLE : '[data-role="select-dropdown"]'
}
const Keyword = {
COUNT_SELECTED : '%count_selected%'
}
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
class SelectDropdown {
constructor(element, config) {
this._multiselect = this._isMultiselect(element)
this._config = this._getConfig(config)
this._element = element
this._prefix =
"bsd" + this._config.SelectDropdownIndex +
"-" +
Math.random()
.toString(36)
.substring(7) +
"-"
this._indexes = []
this._lastSearch = null
this._resultsChanged = false
this._hoverItem = $()
this._keyupTimeout = null
this.ids = {}
this.ids.dropdownContainerId = this._prefix + 'container'
this.ids.dropdownButtonId = this._prefix + 'button'
this.ids.controlSearchId = this._prefix + 'search'
this.ids.dropdownItemDeselect = this._prefix + 'deselect'
this.ids.dropdownItemShowSelected = this._prefix + 'selected'
// Selectors.
this.selectors = {}
if ( this._config.badges ) {
this.selectors.badge = this._classListToSelector( this._config.classBadge )
}
// Properties: Elements.
this.els = {}
this.els.btnSelect = this._buildBtnSelect()
if ( this._config.search ) {
this.els.controlSearch = this._buildControlSearch()
this.els.clear = this._buildControlClear()
}
if ( this._config.deselectAll ) {
this.els.deselectAll = this._buildDeselectAll()
}
if ( this._config.selectAll ) {
this.els.selectAll = this._buildSelectAll()
}
this.els.showSelected = this._buildShowSelected()
this.els.dropdown = this._buildDropdown()
this.els.dropdownMenu = this._buildDropdownMenu() //This should be dropdown menu so we can build and refer to dropdown, the main container.
this.els.dropdownItemsContainer = this._buildDropdownItemsContainer()
this.els.dropdownItemNoResults = this._buildDropdownItemNoResults()
this.els.dropdownItems = this._buildDropdownItems()
this.els.dropdownOptions = this.els.dropdownItems.filter( ( index, element ) => {
return this._isOption( $( element ) )
})
if ( this._config.badges ) {
this.els.badgeContainer = this._buildBadgeContainer()
}
if (this._config.search) {
this._hideClear()
this._haystack = []
this._fuseOptions = {
keys: ['text'],
id: 'index'
}
this.els.dropdownOptions.each( ( index, element ) => {
this._haystack[ index ] = {
index : $( element ).data('index'),
text : $( element ).text()
}
})
}
this._addEventListeners()
this.init()
}
// Getters
static get VERSION() {
return VERSION
}
static get Default() {
return Default
}
// Public
/**
* Select/Deselect a dropdown item, and update the corresponding option.
* @param {object} $dropdownItem jQuery object
* @return {undefined}
*/
toggle( $dropdownItem ) {
var $el = $( this._element )
var itemIndex = $dropdownItem.data('option')
var $option = $el.find('option').eq( itemIndex )
if ( $option.is(':selected') ) {
$option.prop('selected', false)
$dropdownItem.removeClass( ClassName.ACTIVE )
}
else {
if ( !this._multiselect ) {
this.els.dropdownOptions.removeClass( ClassName.ACTIVE )
}
$option.prop('selected', true)
$dropdownItem.removeClass( ClassName.HOVER_BG ).addClass( ClassName.ACTIVE )
}
this._externalFeedback()
}
/**
* Deselect a dropdown item.
* @param {object} $dropdownItem jQuery
* @return {undefined}
*/
deselect( $dropdownItem ){
var $el = $( this._element )
var itemIndex = $dropdownItem.data('option')
var $option = $el.find('option').eq( itemIndex )
if ( $option.is(':selected') ) {
this.toggle( $dropdownItem )
}
}
/**
* Select a dropdown item.
* @param {object} $dropdownItem jQuery
* @return {undefined}
*/
select( $dropdownItem ){
var $el = $( this._element )
var itemIndex = $dropdownItem.data('option')
var $option = $el.find('option').eq( itemIndex )
if ( !$option.is(':selected') ) {
this.toggle( $dropdownItem )
}
}
/**
* Deselect all dropdown items.
* @return {undefined}
*/
deselectAll() {
var $el = $(this._element)
$el.find('option').prop('selected', false)
this.els.dropdownOptions.removeClass( ClassName.ACTIVE )
this._externalFeedback()
this._refresh()
}
/**
* Select all dropdown items.
* @return {undefined}
*/
selectAll() {
var $el = $(this._element)
$el.find('option').prop('selected', true)
this.els.dropdownOptions.removeClass( ClassName.HOVER_BG ).addClass( ClassName.ACTIVE )
this._externalFeedback()
this._refresh()
}
// Private
_getConfig(config) {
if ( config.profile == 'minimal' ) {
if ( typeof config.search !== typeof undefined ) {
config.search = false
}
if ( typeof config.badges !== typeof undefined ) {
config.badges = false
}
if ( typeof config.deselectAll !== typeof undefined ) {
config.deselectAll = false
}
if ( typeof config.selectAll !== typeof undefined ) {
config.selectAll = false
}
if ( typeof config.showSelected !== typeof undefined ) {
config.showSelected = false
}
}
config = {
...Default,
...config
}
Util.typeCheckConfig(NAME, config, DefaultType)
if ( !this._multiselect ) {
config.deselectAll = false
config.selectAll = false
config.showSelected = false
config.badges = false
}
return config
}
_addEventListeners() {
if (this._config.search) {
this.els.controlSearch
.on( Event.KEYUP, ( event ) => {
if (this._config.keyboard) {
this._keyupNav( event )
}
clearTimeout( this._keyupTimeout )
this._keyupTimeout = setTimeout( () => {
let s = $( this.els.controlSearch ).val()
this._search( s )
}, KEYUP_TIMEOUT )
})
this.els.controlSearch
.on( Event.FOCUS, (event) => {
this._hoverSet()
if ( this.els.btnSelect.attr('aria-expanded') == 'false' ) {
this.els.btnSelect.dropdown('toggle')
setTimeout( () => {
this._searchControlFocus()
}, 1)
}
})
this.els.controlSearch
.on(Event.BLUR, (event) => {
this._hoverRemoveAll()
})
// Handle cut and paste.
this.els.controlSearch.bind({
paste (){
$(this).trigger( Event.KEYDOWN )
},
cut (){
$(this).trigger( Event.KEYDOWN )
}
})
}
this._assignClickHandlers()
}
_keyupNav(event) {
if ( event.which == ENTER_KEYCODE ) {
this.toggle( this.els.dropdown.find('.hover').first() )
if ( !this._multiselect ) {
this.els.btnSelect.dropdown('toggle')
}
return
}
else if ( event.which == ARROW_UP_KEYCODE ) {
this._hoverUp()
}
else if ( event.which == ARROW_DOWN_KEYCODE ) {
this._hoverDown()
}
if ( !this._dropdownActive() ) {
this.els.btnSelect.dropdown('toggle')
this._searchControlFocus()
}
}
_assignClickHandlers() {
// Select item.
this.els.dropdownOptions.on( Event.CLICK, ( event ) => {
event.preventDefault()
if ( this._multiselect ) {
this._preventDropdownHide()
}
this.toggle( $( event.currentTarget ) )
})
// Deselect all.
if ( this._config.deselectAll ) {
this.els.deselectAll.on( Event.CLICK, ( event ) => {
event.preventDefault()
this._preventDropdownHide()
if (!$( event.currentTarget ).hasClass('disabled')) {
this.deselectAll()
}
})
}
// Select all.
if ( this._config.selectAll ) {
this.els.selectAll.on( Event.CLICK, ( event ) => {
event.preventDefault()
this._preventDropdownHide()
if (!$( event.currentTarget ).hasClass('disabled')) {
this.selectAll()
}
})
}
// Clear search.
if ( this._config.search ) {
this.els.clear.on( Event.CLICK, ( event ) => {
event.preventDefault()
this.els.controlSearch.val('')
this._preventDropdownHide()
this._refresh()
this._searchControlFocus()
})
}
// Show selected.
this.els.showSelected.on( Event.CLICK, (event) => {
event.preventDefault()
this._preventDropdownHide()
if ( !$( event.currentTarget ).hasClass('disabled') ) {
this._toggleShowSelected()
}
})
// No results.
this.els.dropdownItemNoResults.on( Event.CLICK, (event) => {
event.preventDefault()
this._preventDropdownHide()
})
// Badges.
if ( this._config.badges ) {
this.els.badgeContainer.on( Event.CLICK, 'a', (event) => {
event.preventDefault()
let $target = $( event.currentTarget )
this.deselect( this._dropdownItemByOption( $target.data('option') ) )
$target.parent( this.selectors.badge ).remove()
})
}
}
init() {
// Build.
this._externalFeedback()
// Dropdown menu.
if ( this._config.search ) {
this.els.dropdownMenu
.append( this.els.clear )
}
if ( this._config.selectAll && !this._config.selectButtons ) {
this.els.dropdownMenu
.append( this.els.selectAll )
}
if ( this._config.deselectAll && !this._config.selectButtons ) {
this.els.dropdownMenu
.append( this.els.deselectAll )
}
if ( this._config.showSelected ) {
this.els.dropdownMenu
.append( this.els.showSelected )
}
// Dropdown items.
this.els.dropdownItemsContainer
.append( this.els.dropdownItems )
this.els.dropdownMenu
.append( this.els.dropdownItemsContainer )
// Dropdown.
this.els.dropdown
.append( this.els.dropdownMenu )
// Replace <select>.
let $el = $( this._element )
$el.after( this.els.dropdown )
if ( this._config.hideSelect ) {
$el.hide()
}
if ( this._config.badges ) {
this.els.dropdown.after(
this.els.badgeContainer
)
}
}
/**
* Check whether the supplied element has a `multiple` attribute,
* @param {Object} element
* @return {Boolean}
*/
_isMultiselect(element) {
var attrMultiple = $(element).attr('multiple')
if ( typeof attrMultiple !== typeof undefined && attrMultiple !== false ) {
return true
}
return false
}
/**
* Search and take appropriate action.
*
* * If results haven't changed, do nothing (improves performance).
* * If results have changed, apply changes.
* @param {String} s Search term
* @return {undefined}
*/
_search(s) {
let results = null
if ( $.trim( s ) == '' ) {
this._refresh()
if ( this._lastSearch !== null ) {
this._resultsChanged = true
this._lastSearch = null
this._hoverSet()
}
return
} else {
var fuse = new Fuse( this._haystack, this._fuseOptions )
results = fuse.search(s)
}
this._resultsChanged = true
if ( results ) {
if (
typeof this._lastSearch !== null &&
this._arraysEqual( results, this._lastSearch )
) {
this._resultsChanged = false
}
} else {
this._refresh()
return
}
if ( this._resultsChanged ) {
this._applySearch( results )
}
this._lastSearch = results
}
/**
* Apply changes as per search results.
*
* * Remove showSelected active class if present.
* * Show clear control.
* * Set hover on the first element for keyboard navigation.
* * Hide non-results.
* * Reorder search results.
* * Reset scroll (scroll to top).
* @param {array} results Option index numbers.
* @return {undefined}
*/
_applySearch( results ) {
this._softDisableShowSelected()
this._showClear()
this._hoverSet( results[0] )
this._hide( results )
this._reorder( results )
this._resetScroll()
}
_buildBtnSelect() {
return $('<button>', {
class: this._config.classBtnSelect + ' dropdown-toggle',
type: 'button',
id: this.ids.dropdownButtonId,
'data-toggle': 'dropdown',
'data-target': '#' + this.ids.dropdownContainerId,
'aria-haspopup': 'true',
'aria-expanded': 'false'
})
.text('Select')
}
/**
* Build HTML: Clear control
* @return {object} jQuery
*/
_buildControlClear(){
return $('<a>', {
href: '#',
class: ClassName.ITEM
})
.html( this._config.htmlClear )
}
/**
* Build HTML: Deselect all element
* @return {object} jQuery
*/
_buildDeselectAll() {
let element
if ( !this._config.selectButtons ) {
element = $('<a>', {
href: '#',
class: ClassName.ITEM
})
}
else {
element = $('<button>', {
type: 'button',
class: this._config.classBtnDeselectAll
})
}
return element
.html( this._config.htmlDeselectAll )
.attr('title', 'Deselect all')
}
/**
* Build HTML: Select all element
* @return {object} jQuery
*/
_buildSelectAll() {
let element
if ( !this._config.selectButtons ) {
element = $('<a>', {
href: '#',
class: ClassName.ITEM
})
}
else {
element = $('<button>', {
type: 'button',
class: this._config.classBtnSelectAll
})
}
return element
.html( this._config.htmlSelectAll )
.attr('title', 'Select all')
}
_buildShowSelected() {
var $showSelectedItem = $('<a>', {
href: '#',
class: ClassName.ITEM,
text: 'Show selected'
})
return $showSelectedItem
}
_buildControlSearch() {
return $('<input>', {
type: 'text',
class: 'form-control',
placeholder: 'Search',
'aria-label': 'Search',
'aria-describedby': this.ids.controlSearchId
})
}
_buildDropdown() {
var $dropdown = $('<div>', {
id : this.ids.dropdownContainerId,
class : ClassName.DROPDOWN
})
// Build dropdown.
if ( this._config.search ) {
$dropdown
.append( this._buildDropdownInputGroup() )
}
else if ( this._config.selectButtons ) {
$dropdown
.append( this._buildDropdownBtnGroup() )
}
else {
$dropdown.append( this.els.btnSelect )
}
return $dropdown
}
_buildDropdownInputGroup() {
let $inputGroup = $("<div>", {
class: ClassName.INPUT_GROUP
})
$inputGroup.append(this.els.controlSearch)
if (this._config.selectButtons) {
if (this._config.deselectAll) {
$inputGroup.append(
$("<div>", {
class: ClassName.INPUT_GROUP_APPEND
}).append(this.els.deselectAll)
)
}
if (this._config.selectAll) {
$inputGroup.append(
$("<div>", {
class: ClassName.INPUT_GROUP_APPEND
}).append(this.els.selectAll)
)
}
}
$inputGroup.append(
$("<div>", {
class: ClassName.INPUT_GROUP_APPEND
}).append(this.els.btnSelect)
)
return $inputGroup
}
_buildDropdownBtnGroup() {
let $btnGroup = $("<div>", {
class: ClassName.BTN_GROUP
})
if (this._config.deselectAll) {
$btnGroup.append(this.els.deselectAll)
}
if (this._config.selectAll) {
$btnGroup.append(this.els.selectAll)
}
$btnGroup.append(
$("<div>", {
class: ClassName.BTN_GROUP
}).append(this.els.btnSelect)
)
return $btnGroup
}
_buildDropdownMenu() {
var $dropdownMenu = $('<div>', {
class: ClassName.MENU,
'aria-labelledby': this.ids.dropdownButtonId
})
if ( this._config.maxHeight ) {
$dropdownMenu
.css({
'height': 'auto',
'max-height': this._config.maxHeight,
'overflow-x': 'hidden'
})
}
if ( this._config.search ) {
$dropdownMenu.addClass( ClassName.ALIGNMENT_RIGHT )
}
return $dropdownMenu
}
_buildDropdownItemsContainer() {
return $('<div>')
}
_buildDropdownItemNoResults() {
return $( '<span>', {
class: ClassName.ITEM + ' ' + ClassName.TEXT_MUTED + ' ' + ClassName.BG_TRANSPARENT,
text: this._config.textNoResults
}).hide()
}
_buildDropdownItems() {
let s = 0 // Sort index
let o = 0 // Option index
let $items = $()
let $optgroups = $( this._element ).find('optgroup')
if ( $optgroups.length ) {
$optgroups.each( ( index, element ) => {
$items = $items.add(
this._buildDropdownHeader( $( element ).attr('label') )
.data('index', s )
)
s = this._incrementIndex( s )
$( element ).find('option').each( ( index, element ) => {
$( element ).data('option', o )
$items = $items.add(
this._buildDropdownItem( $( element ) )
.data('index', s )
.data('option', o )
)
s = this._incrementIndex( s )
o++
})
})
} else {
$( this._element ).find('option').each( ( index, element ) => {
$( element ).data('option', o )
$items = $items.add(
this._buildDropdownItem( $( element ) )
.data('index', s )
.data('option', o)
)
s = this._incrementIndex( s )
o++
})
}
if ( this._config.search ) {
$items = $items.add( this.els.dropdownItemNoResults )
}
return $items
}
_incrementIndex( index ) {
this._indexes.push( index.toString() )
index++
return index
}
_buildDropdownHeader( text ) {
return $( '<h6>', {
class: 'dropdown-header',
text: text
})
}
_buildDropdownItem( $option ) {
var $dropdownItem = $( '<a>', {
href: '#',
class: ClassName.ITEM,
text: $option.text()
})
if ( $option.is(':selected') ) {
$dropdownItem.addClass('active')
}
return $dropdownItem
}
_buildBadgeContainer() {
return $('<div>', {
'class' : this._config.classBadgeContainer
})
}
/**
* Build badge.
* @param {Integer} option Option index number
* @param {String} text
* @return {Object} jQuery object
*/
_buildBadge( option, text ) {
let badge = $('<span>',
{
'class' : this._config.classBadge
})
.text( text )
if ( this._config.badgesDismissable ) {
badge
.append(' ')
.append(
$('<a>', {
'href' : '#',
'class' : this._config.classBadgeLink
})
.html( this._config.htmlBadgeRemove )
.data( 'option', option )
)
}
return badge
}
/**
* Boolean: Bootstrap Dropdown visible.
* @return {boolean}
*/
_dropdownActive() {
if (this.els.dropdownMenu.hasClass('show')) {
return true
}
return false
}
/**
* Boolean: Dropdown item is associated with a select option.
*
* e.g. Isn't an `<optgroup>` heading.
* @param {object} $item jQuery object.
* @return {Boolean}
*/
_isOption( $item ) {
var attr = $item.data('option')
if (typeof attr !== typeof undefined && attr !== false) {
return true
}
return false
}
_externalFeedback() {
this._setButtonText()
if ( this._config.badges ) {
this._setBadges()
}
}
/**
* Set button text.
* @return {undefined}
*/
_setButtonText() {
let btnText
let selected = $( this._element ).val()
let noneSelected = false
let allSelected = false
if ( !this._multiselect && selected.length > 0 ) {
btnText = $( this._element ).find('option:selected').text()
}
else if ( selected.length < 1 ) {
btnText = this._config.textNoneSelected
noneSelected = true
}
else if ( selected.length <= this._config.maxListLength ) {
btnText = this._getTextValues().join(", ")
}
else {
btnText = this._config.textMultipleSelected
btnText = btnText.replace( Keyword.COUNT_SELECTED, selected.length )
}
if ( selected.length == this.els.dropdownOptions.length ) {
allSelected = true
}
this._refreshInitialControls( allSelected, noneSelected )
this.els.btnSelect.text( btnText )
}
_setBadges( selected ) {
let badges = $()
let $selected = $( this._element ).find('option:selected')
$selected.each( ( index, element ) => {
badges = badges.add( this._buildBadge( $( element ).data('option'), $( element ).text() ) )
})
this.els.badgeContainer
.html('')
.append( badges )
}
_getText( value ) {
return $( this._element )
.find("option[value='" + value + "']")
.first()
.text()
}
_getTextValues() {
return $( this._element )
.find('option:selected')
.map(function (i, element) {
return $(element).text()
})
.get()
}
/**
* Restore initial dropdown state.
*
* * Remove hover.
* * Hide the 'no results' dropdown item.
* * Reset sort order.
* * Show initial controls.
* @return {undefined}
*/
_refresh() {
this._hideClear()
this.els.dropdownItemNoResults.hide()
this.els.dropdownOptions.show()
this._sortReset()
this._showInitialControls()
}
_hide( results ) {
var notResults = $(this._indexes).not(results).get()
$.each( notResults, ( index, value ) => {
this._dropdownItemByIndex( value ).hide()
})
$.each( results, ( index, value ) => {
this._dropdownItemByIndex( value ).show()
})
this.els.btnSelect.dropdown('update')
}
_dropdownItemByIndex( s ) {
return this.els.dropdownItems.filter( ( index, element ) => {
return $( element ).data('index') == s
})
}
_dropdownItemByOption( o ) {
return this.els.dropdownItems.filter( ( index, element ) => {
return $( element ).data('option') == o
})
}
_hideInitialControls() {
if ( this._config.selectAll && !this._config.selectButtons ) {
this.els.selectAll.hide()
}
if ( this._config.deselectAll && !this._config.selectButtons ) {
this.els.deselectAll.hide()
}
if ( this._config.showSelected ) {
this.els.showSelected.hide()
}
}
_showInitialControls() {
if ( this._config.selectAll && !this._config.selectButtons ) {
this.els.selectAll.show()
}
if ( this._config.deselectAll && !this._config.selectButtons ) {
this.els.deselectAll.show()
}
if ( this._config.showSelected ) {
this.els.showSelected.show()
}
}
_refreshInitialControls( allSelected, noneSelected ) {
if ( this._config.selectAll ) {
this._disableEnable( this.els.selectAll, allSelected )
}
if ( this._config.deselectAll ) {
this._disableEnable( this.els.deselectAll, noneSelected )
}
if ( this._config.showSelected ) {
this._disableEnable( this.els.showSelected, ( noneSelected || allSelected ) )
}
}
_showClear() {
if ( this._config.search ) {
this.els.clear.show()
}
}
_hideClear() {
if ( this._config.search ) {
this.els.clear.hide()
}
}
_disableEnable( $element, condition ) {
if ( condition ) {
this._disable( $element )
}
else {
this._enable( $element )
}
}
_enable( $element ) {
if ( $element.is( 'button' ) ) {
$element.prop('disabled', false )
}
$element.removeClass( ClassName.DISABLED )
if ( $element.is( 'a' ) ) {
$element.removeClass( ClassName.TEXT_MUTED )
}
}
_disable( $element ) {
if ( $element.is( 'button' ) ) {
$element.prop('disabled', true )
}
$element.addClass( ClassName.DISABLED )
if ( $element.is( 'a' ) ) {
$element.addClass( ClassName.TEXT_MUTED )
}
}
/**
* Set hover class position.
*
* Move the hover class to a designated dropdown option. If the index points
* to a non-option, the next option will be modified.
* @param {integer} index Dropdown menu item index.
*/
_hoverSet( index ) {
this._hoverRemoveAll()
if ( typeof index === typeof undefined ) {
var $item = this.els.dropdownOptions.first()
} else {
var $item = this._dropdownItemByIndex( index )
}
this._hoverItem = $item
this._hoverAdd( $item )
}
/**
* Move the hover class up.
*
* @return {undefined}
*/
_hoverUp() {
var current = this._hoverItem
if (
typeof current !== typeof undefined &&
current.length
) {
var prev = current.prevAll('a:visible').first()
}
if ( typeof prev !== typeof undefined && prev.length ) {
this._hoverRemove( current )
this._hoverAdd( prev )
this._hoverItem = prev
}
}
/**
* Move the hover class down.
*
* @return {undefined}
*/
_hoverDown() {
var current = this._hoverItem
if (
typeof current !== typeof undefined &&
current.length
) {
var next = current.nextAll('a:visible').first()
}
if ( typeof next !== typeof undefined && next.length ) {
this._hoverRemove( current )
this._hoverAdd( next )
this._hoverItem = next
}
}
_hoverAdd( $element ) {
let className = ClassName.HOVER
if ( !$element.hasClass( ClassName.ACTIVE ) ) {
className = className + ' ' + ClassName.HOVER_BG
}
$element.addClass( className )
}
_hoverRemove( $element ) {
$element.removeClass( ClassName.HOVER + ' ' + ClassName.HOVER_BG )
}
/**
* Remove hover class from all dropdown options.
* @return {undefined}
*/
_hoverRemoveAll() {
this.els.dropdownOptions.removeClass( ClassName.HOVER + ' ' + ClassName.HOVER_BG )
}
/**
* Sort: Reset sort order.
* @return {undefined}
*/
_sortReset() {
var i
for ( i = this.els.dropdownItems.length; i >= 0; i--) {
this._dropdownItemByIndex( i ).prependTo( this.els.dropdownItemsContainer )
}
}
/**
* Sort: Order by array values.
*
* reorder according to an array of index values. The order of the index
* array is preserved, to make change detection easier elsewhere.
* @param {array} indexes Array of index values (strings).
* @return {undefined}
*/
_reorder( indexes ) {
this.els.dropdownItemNoResults.hide()
if ( typeof indexes === typeof undefined || indexes.length == 0) {
this.els.dropdownItemNoResults.show()
return
}
var indexesReversed = indexes.slice(0) // Clone
indexesReversed = indexesReversed.reverse()
$.each( indexesReversed, function( index, value ) {
this._dropdownItemByIndex( value ).prependTo( this.els.dropdownItemsContainer )
})
this._hideInitialControls()
}
/**
* Sort: Move selected items to the top.
* @return {undefined}
*/
_toggleShowSelected() {
if ( this.els.showSelected.hasClass( ClassName.ACTIVE ) ) {
this.disableShowSelected()
}
else {
this._enableShowSelected()
}
}
_enableShowSelected() {
this.els.showSelected.addClass( ClassName.ACTIVE )
$( this.els.dropdownItemsContainer.find('.active').get().reverse() ).each( ( index, element ) => {
$( element ).prependTo( this.els.dropdownItemsContainer )
})
this._resetScroll()
}
_disableShowSelected() {
this._softDisableShowSelected()
this._sortReset()
}
_softDisableShowSelected(){
this.els.showSelected.removeClass( ClassName.ACTIVE )
}
_preventDropdownHide() {
if ( this._dropdownActive() ) {
this.els.dropdown.one('hide.bs.dropdown', function ( event ) {
event.preventDefault()
})
}
}
_searchControlFocus() {
this.els.controlSearch
.focus()
.val( this.els.controlSearch.val() )
}
/**
* Helper: Reset scroll.
*
* Scroll the dropdown menu to the top.
* @return {[type]} [description]
*/
_resetScroll() {
this.els.dropdownMenu.scrollTop(0)
}
/**
* Helper: Class to selector.
*
* Convert a space separated class list to a selector.
* @param {string} classList Space separated list of classes.
* @return {string} Selector.
*/
_classListToSelector( classList ) {
let selector = classList
if ( classList.length ) {
let classes = classList.split(/\s+/)
selector = '.' + classes.join('.')
}
return selector
}
/**
* Helper: Compare two arrays.
*
* @see https://stackoverflow.com/questions/3115982/how-to-check-if-two-arrays-are-equal-with-javascript
*/
_arraysEqual( a, b ) {
if (a === b) return true
if (a == null || b == null) return false
if (a.length != b.length) return false
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false
}
return true
}
// Static
static _jQueryInterface(config, relatedTarget) {
return this.each(function () {
let data = $(this).data(DATA_KEY)
const _config = {
...SelectDropdown.Default,
...$(this).data(),
...typeof config === 'object' && config,
SelectDropdownIndex
}
SelectDropdownIndex++
if (!data) {
data = new SelectDropdown(this, _config)
$(this).data(DATA_KEY, data)
}
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config](relatedTarget)
} else if (_config.show) {
data.show(relatedTarget)
}
})
}
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/
$(window).on(Event.LOAD_DATA_API, () => {
$(Selector.DATA_ROLE).each(function () {
const $selectDropdown = $(this)
SelectDropdown._jQueryInterface.call($selectDropdown, $selectDropdown.data())
})
})
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
*/
$.fn[NAME] = SelectDropdown._jQueryInterface
$.fn[NAME].Constructor = SelectDropdown
$.fn[NAME].noConflict = function () {
$.fn[NAME] = JQUERY_NO_CONFLICT
return SelectDropdown._jQueryInterface
}
return SelectDropdown
})($, Fuse)
export default SelectDropdown