2021-12-17 22:26:52 +01:00
import merge from 'deepmerge' ;
2019-12-23 19:22:54 +01:00
/* eslint-disable @typescript-eslint/no-explicit-any */
2016-05-03 15:55:38 +02:00
import Fuse from 'fuse.js' ;
2018-05-25 10:01:55 +02:00
2018-05-28 14:55:44 +02:00
import {
2021-12-17 22:26:52 +01:00
activateChoices ,
addChoice ,
clearChoices ,
filterChoices ,
Result ,
} from './actions/choices' ;
import { addGroup } from './actions/groups' ;
import { addItem , highlightItem , removeItem } from './actions/items' ;
import { clearAll , resetTo , setIsLoading } from './actions/misc' ;
import {
2018-05-28 14:55:44 +02:00
Container ,
2021-12-17 22:26:52 +01:00
Dropdown ,
2018-05-28 14:55:44 +02:00
Input ,
List ,
WrappedInput ,
WrappedSelect ,
} from './components' ;
2019-11-03 14:18:16 +01:00
import {
EVENTS ,
KEY_CODES ,
SELECT_MULTIPLE_TYPE ,
2021-12-17 22:26:52 +01:00
SELECT_ONE_TYPE ,
TEXT_TYPE ,
2019-11-03 14:18:16 +01:00
} from './constants' ;
2021-12-17 22:26:52 +01:00
import { DEFAULT_CONFIG } from './defaults' ;
import { Choice } from './interfaces/choice' ;
import { Group } from './interfaces/group' ;
import { Item } from './interfaces/item' ;
import { Notice } from './interfaces/notice' ;
import { Options } from './interfaces/options' ;
import { PassedElement } from './interfaces/passed-element' ;
import { State } from './interfaces/state' ;
2016-08-14 23:14:37 +02:00
import {
2021-12-17 22:26:52 +01:00
diff ,
existsInArray ,
generateId ,
2016-09-05 23:04:15 +02:00
getAdjacentEl ,
2017-02-17 10:23:52 +01:00
getType ,
2021-12-17 22:26:52 +01:00
isScrolledIntoView ,
2016-09-05 23:04:15 +02:00
isType ,
sortByScore ,
2021-12-17 22:26:52 +01:00
strToEl ,
2017-08-16 13:40:09 +02:00
} from './lib/utils' ;
2019-12-23 19:22:54 +01:00
import { defaultState } from './reducers' ;
2021-12-17 22:26:52 +01:00
import Store from './store/store' ;
import templates from './templates' ;
2016-08-14 23:14:37 +02:00
2019-11-12 10:47:41 +01:00
/** @see {@link http://browserhacks.com/#hack-acea075d0ac6954f275a70023906050c} */
const IS_IE11 =
'-ms-scroll-limit' in document . documentElement . style &&
'-ms-ime-align' in document . documentElement . style ;
2019-12-23 19:22:54 +01:00
const USER_DEFAULTS : Partial < Options > = { } ;
2019-11-03 14:18:16 +01:00
2019-10-29 19:12:32 +01:00
/ * *
2019-11-03 14:18:16 +01:00
* Choices
* @author Josh Johnson < josh @ joshuajohnson.co.uk >
2019-10-29 19:12:32 +01:00
* /
2021-12-17 22:26:52 +01:00
class Choices implements Choices {
2019-12-23 19:22:54 +01:00
static get defaults ( ) : {
options : Partial < Options > ;
templates : typeof templates ;
} {
2019-10-29 16:13:00 +01:00
return Object . preventExtensions ( {
2019-12-23 19:22:54 +01:00
get options ( ) : Partial < Options > {
2019-10-29 16:13:00 +01:00
return USER_DEFAULTS ;
} ,
2019-12-23 19:22:54 +01:00
get templates ( ) : typeof templates {
return templates ;
2019-10-29 16:13:00 +01:00
} ,
} ) ;
}
2019-12-23 19:22:54 +01:00
initialised : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
config : Options ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
passedElement : WrappedInput | WrappedSelect ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
containerOuter : Container ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
containerInner : Container ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
choiceList : List ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
itemList : List ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
input : Input ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
dropdown : Dropdown ;
_isTextElement : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_isSelectOneElement : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_isSelectMultipleElement : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_isSelectElement : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_store : Store ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_templates : typeof templates ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_initialState : State ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_currentState : State ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_prevState : State ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_currentValue : string ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_canSearch : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_isScrollingOnIe : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_highlightPosition : number ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_wasTap : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_isSearching : boolean ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_placeholderValue : string | null ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_baseId : string ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_direction : HTMLElement [ 'dir' ] ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_idNames : {
itemChoice : string ;
} ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_presetGroups : Group [ ] | HTMLOptGroupElement [ ] | Element [ ] ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_presetOptions : Item [ ] | HTMLOptionElement [ ] ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_presetChoices : Partial < Choice > [ ] ;
2021-12-17 22:26:52 +01:00
2019-12-23 19:22:54 +01:00
_presetItems : Item [ ] | string [ ] ;
constructor (
element :
| string
| Element
| HTMLInputElement
| HTMLSelectElement = '[data-choice]' ,
userConfig : Partial < Options > = { } ,
) {
2021-12-23 17:59:48 +01:00
if ( userConfig . allowHTML === undefined ) {
2021-12-24 18:32:50 +01:00
console . warn (
2021-12-26 15:36:12 +01:00
'Deprecation warning: allowHTML will default to false in a future release. To render HTML in Choices, you will need to set it to true. Setting allowHTML will suppress this message.' ,
2021-12-24 18:32:50 +01:00
) ;
2021-12-23 17:59:48 +01:00
}
2019-12-23 19:22:54 +01:00
this . config = merge . all < Options > (
2019-10-29 16:13:00 +01:00
[ DEFAULT_CONFIG , Choices . defaults . options , userConfig ] ,
2019-01-19 15:47:22 +01:00
// When merging array configs, replace with a copy of the userConfig array,
// instead of concatenating with the default array
2019-11-03 14:18:16 +01:00
{ arrayMerge : ( _ , sourceArray ) = > [ . . . sourceArray ] } ,
2019-01-19 15:47:22 +01:00
) ;
2016-09-05 23:04:15 +02:00
2019-02-12 19:35:46 +01:00
const invalidConfigOptions = diff ( this . config , DEFAULT_CONFIG ) ;
if ( invalidConfigOptions . length ) {
console . warn (
'Unknown config option(s) passed' ,
invalidConfigOptions . join ( ', ' ) ,
) ;
2018-10-09 14:16:58 +02:00
}
2019-10-29 22:19:56 +01:00
const passedElement =
typeof element === 'string' ? document . querySelector ( element ) : element ;
2019-10-22 13:50:40 +02:00
2019-10-29 22:19:56 +01:00
if (
! (
passedElement instanceof HTMLInputElement ||
passedElement instanceof HTMLSelectElement
)
) {
throw TypeError (
'Expected one of the following types text|select-one|select-multiple' ,
) ;
2018-10-11 20:33:19 +02:00
}
2019-11-03 14:18:16 +01:00
this . _isTextElement = passedElement . type === TEXT_TYPE ;
this . _isSelectOneElement = passedElement . type === SELECT_ONE_TYPE ;
this . _isSelectMultipleElement = passedElement . type === SELECT_MULTIPLE_TYPE ;
2018-05-28 14:55:44 +02:00
this . _isSelectElement =
this . _isSelectOneElement || this . _isSelectMultipleElement ;
2017-10-13 14:43:58 +02:00
2019-11-03 19:12:47 +01:00
this . config . searchEnabled =
this . _isSelectMultipleElement || this . config . searchEnabled ;
2019-12-23 19:22:54 +01:00
if ( ! [ 'auto' , 'always' ] . includes ( ` ${ this . config . renderSelectedChoices } ` ) ) {
2019-11-03 19:12:47 +01:00
this . config . renderSelectedChoices = 'auto' ;
}
if (
userConfig . addItemFilter &&
typeof userConfig . addItemFilter !== 'function'
) {
const re =
userConfig . addItemFilter instanceof RegExp
? userConfig . addItemFilter
: new RegExp ( userConfig . addItemFilter ) ;
this . config . addItemFilter = re . test . bind ( re ) ;
}
2018-05-24 10:22:07 +02:00
if ( this . _isTextElement ) {
2018-05-21 18:01:03 +02:00
this . passedElement = new WrappedInput ( {
2019-12-23 19:22:54 +01:00
element : passedElement as HTMLInputElement ,
2018-05-21 18:01:03 +02:00
classNames : this.config.classNames ,
delimiter : this.config.delimiter ,
} ) ;
2019-10-29 22:19:56 +01:00
} else {
2018-05-21 18:01:03 +02:00
this . passedElement = new WrappedSelect ( {
2019-12-23 19:22:54 +01:00
element : passedElement as HTMLSelectElement ,
2018-05-21 18:01:03 +02:00
classNames : this.config.classNames ,
2019-12-23 19:22:54 +01:00
template : ( data : Item ) : HTMLOptionElement = >
this . _templates . option ( data ) ,
2018-05-21 18:01:03 +02:00
} ) ;
2017-10-13 14:43:58 +02:00
}
2017-08-03 12:44:06 +02:00
2018-05-27 12:57:21 +02:00
this . initialised = false ;
2018-05-29 16:46:30 +02:00
2019-10-29 22:19:56 +01:00
this . _store = new Store ( ) ;
2019-12-23 19:22:54 +01:00
this . _initialState = defaultState ;
this . _currentState = defaultState ;
this . _prevState = defaultState ;
2018-05-27 12:57:21 +02:00
this . _currentValue = '' ;
2019-12-23 19:22:54 +01:00
this . _canSearch = ! ! this . config . searchEnabled ;
2018-05-27 12:57:21 +02:00
this . _isScrollingOnIe = false ;
2018-05-24 10:22:07 +02:00
this . _highlightPosition = 0 ;
2018-05-27 12:57:21 +02:00
this . _wasTap = true ;
2018-05-24 10:22:07 +02:00
this . _placeholderValue = this . _generatePlaceholderValue ( ) ;
2018-05-27 12:57:21 +02:00
this . _baseId = generateId ( this . passedElement . element , 'choices-' ) ;
2019-12-23 19:22:54 +01:00
2019-10-29 15:02:24 +01:00
/ * *
* setting direction in cases where it ' s explicitly set on passedElement
* or when calculated direction is different from the document
* /
2019-11-03 14:18:16 +01:00
this . _direction = this . passedElement . dir ;
2019-11-02 14:49:33 +01:00
2019-10-29 15:02:24 +01:00
if ( ! this . _direction ) {
const { direction : elementDirection } = window . getComputedStyle (
this . passedElement . element ,
) ;
const { direction : documentDirection } = window . getComputedStyle (
document . documentElement ,
) ;
if ( elementDirection !== documentDirection ) {
this . _direction = elementDirection ;
}
}
2019-11-02 14:49:33 +01:00
2018-05-27 12:57:21 +02:00
this . _idNames = {
itemChoice : 'item-choice' ,
} ;
2019-12-23 19:22:54 +01:00
if ( this . _isSelectElement ) {
// Assign preset groups from passed element
this . _presetGroups = ( this . passedElement as WrappedSelect ) . optionGroups ;
// Assign preset options from passed element
this . _presetOptions = ( this . passedElement as WrappedSelect ) . options ;
}
2017-07-11 07:36:10 +02:00
// Assign preset choices from passed object
2018-05-24 10:22:07 +02:00
this . _presetChoices = this . config . choices ;
2016-09-05 23:04:15 +02:00
// Assign preset items from passed object first
2018-05-24 10:22:07 +02:00
this . _presetItems = this . config . items ;
2019-11-03 18:45:16 +01:00
// Add any values passed from attribute
2019-12-23 19:22:54 +01:00
if ( this . passedElement . value && this . _isTextElement ) {
const splitValues : string [ ] = this . passedElement . value . split (
this . config . delimiter ,
2017-05-18 10:29:18 +02:00
) ;
2019-12-23 19:22:54 +01:00
this . _presetItems = ( this . _presetItems as string [ ] ) . concat ( splitValues ) ;
2016-09-05 23:04:15 +02:00
}
2019-11-03 18:45:16 +01:00
// Create array of choices from option elements
2019-12-23 19:22:54 +01:00
if ( ( this . passedElement as WrappedSelect ) . options ) {
2021-12-17 22:26:52 +01:00
( this . passedElement as WrappedSelect ) . options . forEach ( ( option ) = > {
2019-11-03 18:45:16 +01:00
this . _presetChoices . push ( {
2019-12-23 19:22:54 +01:00
value : option.value ,
label : option.innerHTML ,
selected : ! ! option . selected ,
disabled : option.disabled || option . parentNode . disabled ,
placeholder :
option . value === '' || option . hasAttribute ( 'placeholder' ) ,
customProperties : option.dataset [ 'custom-properties' ] ,
2019-11-03 18:45:16 +01:00
} ) ;
} ) ;
}
2018-10-09 13:26:47 +02:00
this . _render = this . _render . bind ( this ) ;
2016-09-05 23:04:15 +02:00
this . _onFocus = this . _onFocus . bind ( this ) ;
this . _onBlur = this . _onBlur . bind ( this ) ;
this . _onKeyUp = this . _onKeyUp . bind ( this ) ;
this . _onKeyDown = this . _onKeyDown . bind ( this ) ;
this . _onClick = this . _onClick . bind ( this ) ;
this . _onTouchMove = this . _onTouchMove . bind ( this ) ;
this . _onTouchEnd = this . _onTouchEnd . bind ( this ) ;
this . _onMouseDown = this . _onMouseDown . bind ( this ) ;
this . _onMouseOver = this . _onMouseOver . bind ( this ) ;
2018-05-28 17:22:22 +02:00
this . _onFormReset = this . _onFormReset . bind ( this ) ;
2019-12-23 19:22:54 +01:00
this . _onSelectKey = this . _onSelectKey . bind ( this ) ;
2018-10-09 14:17:11 +02:00
this . _onEnterKey = this . _onEnterKey . bind ( this ) ;
this . _onEscapeKey = this . _onEscapeKey . bind ( this ) ;
this . _onDirectionKey = this . _onDirectionKey . bind ( this ) ;
this . _onDeleteKey = this . _onDeleteKey . bind ( this ) ;
2016-09-05 23:04:15 +02:00
2019-10-29 22:19:56 +01:00
// If element has already been initialised with Choices, fail silently
2019-11-03 14:18:16 +01:00
if ( this . passedElement . isActive ) {
2019-10-29 22:19:56 +01:00
if ( ! this . config . silent ) {
2019-10-22 13:50:40 +02:00
console . warn (
'Trying to initialise Choices on element already initialised' ,
2019-11-27 12:46:40 +01:00
{ element } ,
2019-10-22 13:50:40 +02:00
) ;
}
2019-10-29 22:19:56 +01:00
this . initialised = true ;
return ;
2017-05-18 18:56:29 +02:00
}
2016-09-05 23:04:15 +02:00
2018-05-23 14:09:45 +02:00
// Let's go
this . init ( ) ;
2016-09-05 23:04:15 +02:00
}
2016-09-04 23:23:20 +02:00
2019-12-23 19:22:54 +01:00
init ( ) : void {
2017-08-30 14:04:19 +02:00
if ( this . initialised ) {
2017-05-18 10:36:33 +02:00
return ;
}
2016-09-05 23:31:20 +02:00
this . _createTemplates ( ) ;
2018-04-24 14:57:31 +02:00
this . _createElements ( ) ;
2017-11-07 15:08:55 +01:00
this . _createStructure ( ) ;
2018-10-30 23:04:08 +01:00
2018-10-09 13:26:47 +02:00
this . _store . subscribe ( this . _render ) ;
2019-12-23 19:22:54 +01:00
2018-10-09 13:26:47 +02:00
this . _render ( ) ;
2016-09-05 23:31:20 +02:00
this . _addEventListeners ( ) ;
2018-10-09 13:43:25 +02:00
2018-10-30 23:04:08 +01:00
const shouldDisable =
! this . config . addItems ||
this . passedElement . element . hasAttribute ( 'disabled' ) ;
if ( shouldDisable ) {
this . disable ( ) ;
}
2018-05-29 16:46:30 +02:00
this . initialised = true ;
2016-05-02 22:39:33 +02:00
2018-05-24 10:22:07 +02:00
const { callbackOnInit } = this . config ;
2016-09-05 23:31:20 +02:00
// Run callback if it is a function
2019-10-29 22:19:56 +01:00
if ( callbackOnInit && typeof callbackOnInit === 'function' ) {
2018-05-24 10:22:07 +02:00
callbackOnInit . call ( this ) ;
2016-03-17 16:00:22 +01:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
destroy ( ) : void {
2017-08-30 14:04:19 +02:00
if ( ! this . initialised ) {
2017-05-18 10:36:33 +02:00
return ;
}
2016-09-05 23:04:15 +02:00
2016-09-05 23:31:20 +02:00
this . _removeEventListeners ( ) ;
2017-10-13 14:43:58 +02:00
this . passedElement . reveal ( ) ;
2017-12-11 15:40:38 +01:00
this . containerOuter . unwrap ( this . passedElement . element ) ;
2016-10-22 21:15:28 +02:00
2019-11-17 13:34:34 +01:00
this . clearStore ( ) ;
2018-05-24 10:22:07 +02:00
if ( this . _isSelectElement ) {
2019-12-23 19:22:54 +01:00
( this . passedElement as WrappedSelect ) . options = this . _presetOptions ;
2017-12-18 13:06:38 +01:00
}
2019-12-23 19:22:54 +01:00
this . _templates = templates ;
2016-09-05 23:31:20 +02:00
this . initialised = false ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
enable ( ) : this {
2018-10-27 21:16:46 +02:00
if ( this . passedElement . isDisabled ) {
this . passedElement . enable ( ) ;
2017-11-07 13:00:10 +01:00
}
if ( this . containerOuter . isDisabled ) {
this . _addEventListeners ( ) ;
this . input . enable ( ) ;
this . containerOuter . enable ( ) ;
}
return this ;
}
2019-12-23 19:22:54 +01:00
disable ( ) : this {
2018-10-27 21:16:46 +02:00
if ( ! this . passedElement . isDisabled ) {
this . passedElement . disable ( ) ;
2017-11-07 13:00:10 +01:00
}
if ( ! this . containerOuter . isDisabled ) {
this . _removeEventListeners ( ) ;
this . input . disable ( ) ;
this . containerOuter . disable ( ) ;
}
return this ;
}
2019-12-23 19:22:54 +01:00
highlightItem ( item : Item , runEvent = true ) : this {
if ( ! item || ! item . id ) {
2017-07-13 16:59:33 +02:00
return this ;
2017-05-18 10:36:33 +02:00
}
2017-11-07 13:51:43 +01:00
const { id , groupId = - 1 , value = '' , label = '' } = item ;
2018-05-24 10:22:07 +02:00
const group = groupId >= 0 ? this . _store . getGroupById ( groupId ) : null ;
2017-01-01 16:32:09 +01:00
2018-05-24 10:22:07 +02:00
this . _store . dispatch ( highlightItem ( id , true ) ) ;
2016-09-05 23:04:15 +02:00
2017-01-01 16:32:09 +01:00
if ( runEvent ) {
2017-11-07 13:51:43 +01:00
this . passedElement . triggerEvent ( EVENTS . highlightItem , {
2017-08-30 14:04:19 +02:00
id ,
2017-11-07 13:51:43 +01:00
value ,
label ,
groupValue : group && group . value ? group.value : null ,
} ) ;
2016-06-08 15:45:29 +02:00
}
2016-08-14 23:14:37 +02:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
unhighlightItem ( item : Item ) : this {
if ( ! item || ! item . id ) {
2017-07-13 16:59:33 +02:00
return this ;
2017-05-18 10:36:33 +02:00
}
2017-11-07 13:51:43 +01:00
const { id , groupId = - 1 , value = '' , label = '' } = item ;
2018-05-24 10:22:07 +02:00
const group = groupId >= 0 ? this . _store . getGroupById ( groupId ) : null ;
2016-10-19 08:33:38 +02:00
2018-05-24 10:22:07 +02:00
this . _store . dispatch ( highlightItem ( id , false ) ) ;
2017-11-07 13:51:43 +01:00
this . passedElement . triggerEvent ( EVENTS . highlightItem , {
id ,
value ,
label ,
groupValue : group && group . value ? group.value : null ,
} ) ;
2016-08-14 23:14:37 +02:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
highlightAll ( ) : this {
2021-12-17 22:26:52 +01:00
this . _store . items . forEach ( ( item ) = > this . highlightItem ( item ) ) ;
2019-10-29 19:26:11 +01:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
unhighlightAll ( ) : this {
2021-12-17 22:26:52 +01:00
this . _store . items . forEach ( ( item ) = > this . unhighlightItem ( item ) ) ;
2019-10-29 19:26:11 +01:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
removeActiveItemsByValue ( value : string ) : this {
2018-05-24 10:22:07 +02:00
this . _store . activeItems
2021-12-17 22:26:52 +01:00
. filter ( ( item ) = > item . value === value )
. forEach ( ( item ) = > this . _removeItem ( item ) ) ;
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
removeActiveItems ( excludedId : number ) : this {
2018-05-24 10:22:07 +02:00
this . _store . activeItems
2018-05-23 14:09:45 +02:00
. filter ( ( { id } ) = > id !== excludedId )
2021-12-17 22:26:52 +01:00
. forEach ( ( item ) = > this . _removeItem ( item ) ) ;
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
removeHighlightedItems ( runEvent = false ) : this {
2021-12-17 22:26:52 +01:00
this . _store . highlightedActiveItems . forEach ( ( item ) = > {
2017-11-21 15:10:29 +01:00
this . _removeItem ( item ) ;
// If this action was performed by the user
// trigger the event
if ( runEvent ) {
this . _triggerChange ( item . value ) ;
2016-09-05 23:04:15 +02:00
}
} ) ;
return this ;
}
2019-12-23 19:22:54 +01:00
showDropdown ( preventInputFocus? : boolean ) : this {
2017-08-30 14:04:19 +02:00
if ( this . dropdown . isActive ) {
return this ;
}
2018-05-23 14:09:45 +02:00
requestAnimationFrame ( ( ) = > {
this . dropdown . show ( ) ;
2019-10-30 18:28:15 +01:00
this . containerOuter . open ( this . dropdown . distanceFromTopWindow ) ;
2017-12-10 17:41:39 +01:00
2018-05-29 16:46:30 +02:00
if ( ! preventInputFocus && this . _canSearch ) {
2018-05-23 14:09:45 +02:00
this . input . focus ( ) ;
}
2017-12-10 17:41:39 +01:00
2018-05-23 14:09:45 +02:00
this . passedElement . triggerEvent ( EVENTS . showDropdown , { } ) ;
} ) ;
2016-06-08 15:45:29 +02:00
2016-09-05 23:04:15 +02:00
return this ;
}
2016-08-04 14:21:24 +02:00
2019-12-23 19:22:54 +01:00
hideDropdown ( preventInputBlur? : boolean ) : this {
2017-08-30 14:04:19 +02:00
if ( ! this . dropdown . isActive ) {
return this ;
}
2018-05-23 14:09:45 +02:00
requestAnimationFrame ( ( ) = > {
this . dropdown . hide ( ) ;
this . containerOuter . close ( ) ;
2017-12-10 17:41:39 +01:00
2018-05-29 16:46:30 +02:00
if ( ! preventInputBlur && this . _canSearch ) {
2018-05-23 14:09:45 +02:00
this . input . removeActiveDescendant ( ) ;
this . input . blur ( ) ;
}
2017-12-10 17:41:39 +01:00
2018-05-23 14:09:45 +02:00
this . passedElement . triggerEvent ( EVENTS . hideDropdown , { } ) ;
} ) ;
2017-10-12 17:27:23 +02:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
getValue ( valueOnly = false ) : string [ ] | Item [ ] | Item | string {
const values = this . _store . activeItems . reduce < any [ ] > (
( selectedItems , item ) = > {
const itemValue = valueOnly ? item.value : item ;
selectedItems . push ( itemValue ) ;
2019-10-29 19:26:11 +01:00
2019-12-23 19:22:54 +01:00
return selectedItems ;
} ,
[ ] ,
) ;
2017-11-07 13:00:10 +01:00
2018-05-24 10:22:07 +02:00
return this . _isSelectOneElement ? values [ 0 ] : values ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
setValue ( items : string [ ] | Item [ ] ) : this {
2017-08-15 17:57:29 +02:00
if ( ! this . initialised ) {
return this ;
}
2017-05-18 10:36:33 +02:00
2021-12-17 22:26:52 +01:00
items . forEach ( ( value ) = > this . _setChoiceOrItem ( value ) ) ;
2019-10-29 19:26:11 +01:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
setChoiceByValue ( value : string ) : this {
2018-05-24 10:22:07 +02:00
if ( ! this . initialised || this . _isTextElement ) {
2017-08-15 17:57:29 +02:00
return this ;
}
// If only one value has been passed, convert to array
2019-10-29 22:19:56 +01:00
const choiceValue = Array . isArray ( value ) ? value : [ value ] ;
2017-10-12 17:27:23 +02:00
// Loop through each value and
2021-12-17 22:26:52 +01:00
choiceValue . forEach ( ( val ) = > this . _findAndSelectChoiceByValue ( val ) ) ;
2017-08-30 14:04:19 +02:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-10-29 19:12:32 +01:00
/ * *
* Set choices of select input via an array of objects ( or function that returns array of object or promise of it ) ,
* a value field name and a label field name .
* This behaves the same as passing items via the choices option but can be called after initialising Choices .
* This can also be used to add groups of choices ( see example 2 ) ; Optionally pass a true ` replaceChoices ` value to remove any existing choices .
* Optionally pass a ` customProperties ` object to add additional data to your choices ( useful when searching / filtering etc ) .
*
* * * Input types affected : * * select - one , select - multiple
*
* @example
* ` ` ` js
* const example = new Choices ( element ) ;
*
* example . setChoices ( [
* { value : 'One' , label : 'Label One' , disabled : true } ,
* { value : 'Two' , label : 'Label Two' , selected : true } ,
* { value : 'Three' , label : 'Label Three' } ,
* ] , 'value' , 'label' , false ) ;
* ` ` `
*
* @example
* ` ` ` js
* const example = new Choices ( element ) ;
*
* example . setChoices ( async ( ) = > {
* try {
* const items = await fetch ( '/items' ) ;
* return items . json ( )
* } catch ( err ) {
* console . error ( err )
* }
* } ) ;
* ` ` `
*
* @example
* ` ` ` js
* const example = new Choices ( element ) ;
*
* example . setChoices ( [ {
* label : 'Group one' ,
* id : 1 ,
* disabled : false ,
* choices : [
* { value : 'Child One' , label : 'Child One' , selected : true } ,
* { value : 'Child Two' , label : 'Child Two' , disabled : true } ,
* { value : 'Child Three' , label : 'Child Three' } ,
* ]
* } ,
* {
* label : 'Group two' ,
* id : 2 ,
* disabled : false ,
* choices : [
* { value : 'Child Four' , label : 'Child Four' , disabled : true } ,
* { value : 'Child Five' , label : 'Child Five' } ,
* { value : 'Child Six' , label : 'Child Six' , customProperties : {
* description : 'Custom description about child six' ,
* random : 'Another random custom property'
* } } ,
* ]
* } ] , 'value' , 'label' , false ) ;
* ` ` `
* /
setChoices (
2019-12-23 19:22:54 +01:00
choicesArrayOrFetcher :
| Choice [ ]
| Group [ ]
| ( ( instance : Choices ) = > Choice [ ] | Promise < Choice [ ] > ) = [ ] ,
2019-10-29 19:12:32 +01:00
value = 'value' ,
label = 'label' ,
replaceChoices = false ,
2019-12-23 19:22:54 +01:00
) : this | Promise < this > {
2019-10-29 19:26:11 +01:00
if ( ! this . initialised ) {
2019-10-29 19:12:32 +01:00
throw new ReferenceError (
` setChoices was called on a non-initialized instance of Choices ` ,
) ;
2019-10-29 19:26:11 +01:00
}
if ( ! this . _isSelectElement ) {
2019-10-29 19:12:32 +01:00
throw new TypeError ( ` setChoices can't be used with INPUT based Choices ` ) ;
2019-10-29 19:26:11 +01:00
}
2019-10-29 19:12:32 +01:00
if ( typeof value !== 'string' || ! value ) {
throw new TypeError (
` value parameter must be a name of 'value' field in passed objects ` ,
) ;
2017-08-15 17:57:29 +02:00
}
// Clear choices if needed
if ( replaceChoices ) {
2019-03-13 10:05:38 +01:00
this . clearChoices ( ) ;
2017-08-15 17:57:29 +02:00
}
2019-11-02 13:58:18 +01:00
if ( typeof choicesArrayOrFetcher === 'function' ) {
// it's a choices fetcher function
2019-10-29 19:12:32 +01:00
const fetcher = choicesArrayOrFetcher ( this ) ;
2019-11-03 18:45:16 +01:00
2019-11-02 13:58:18 +01:00
if ( typeof Promise === 'function' && fetcher instanceof Promise ) {
2019-10-29 19:12:32 +01:00
// that's a promise
2021-12-17 22:26:52 +01:00
// eslint-disable-next-line no-promise-executor-return
return new Promise ( ( resolve ) = > requestAnimationFrame ( resolve ) )
2019-11-02 13:58:18 +01:00
. then ( ( ) = > this . _handleLoadingState ( true ) )
. then ( ( ) = > fetcher )
2019-12-23 19:22:54 +01:00
. then ( ( data : Choice [ ] ) = >
this . setChoices ( data , value , label , replaceChoices ) ,
)
2021-12-17 22:26:52 +01:00
. catch ( ( err ) = > {
2019-10-29 19:26:11 +01:00
if ( ! this . config . silent ) {
console . error ( err ) ;
}
2019-10-29 19:12:32 +01:00
} )
. then ( ( ) = > this . _handleLoadingState ( false ) )
. then ( ( ) = > this ) ;
}
2019-11-02 13:58:18 +01:00
2019-10-29 19:12:32 +01:00
// function returned something else than promise, let's check if it's an array of choices
2019-10-29 19:26:11 +01:00
if ( ! Array . isArray ( fetcher ) ) {
2019-10-29 19:12:32 +01:00
throw new TypeError (
` .setChoices first argument function must return either array of choices or Promise, got: ${ typeof fetcher } ` ,
) ;
2019-10-29 19:26:11 +01:00
}
2019-10-29 19:12:32 +01:00
// recursion with results, it's sync and choices were cleared already
return this . setChoices ( fetcher , value , label , false ) ;
}
2019-11-02 13:58:18 +01:00
if ( ! Array . isArray ( choicesArrayOrFetcher ) ) {
throw new TypeError (
` .setChoices must be called either with array of choices with a function resulting into Promise of array of choices ` ,
) ;
}
2017-11-21 15:10:29 +01:00
this . containerOuter . removeLoadingState ( ) ;
2019-10-29 19:26:11 +01:00
2019-11-08 10:19:18 +01:00
this . _startLoading ( ) ;
2019-11-03 18:45:16 +01:00
2019-12-23 19:22:54 +01:00
type ChoiceGroup = {
id : string ;
choices : Choice [ ] ;
} ;
choicesArrayOrFetcher . forEach ( ( groupOrChoice : ChoiceGroup | Choice ) = > {
if ( ( groupOrChoice as ChoiceGroup ) . choices ) {
2018-05-29 20:55:33 +02:00
this . _addGroup ( {
2019-12-23 19:22:54 +01:00
id : groupOrChoice.id ? parseInt ( ` ${ groupOrChoice . id } ` , 10 ) : null ,
2018-05-29 20:55:33 +02:00
group : groupOrChoice ,
valueKey : value ,
labelKey : label ,
} ) ;
2017-11-21 15:10:29 +01:00
} else {
2019-12-23 19:22:54 +01:00
const choice = groupOrChoice as Choice ;
2018-05-29 20:55:33 +02:00
this . _addChoice ( {
2019-12-23 19:22:54 +01:00
value : choice [ value ] ,
label : choice [ label ] ,
isSelected : ! ! choice . selected ,
isDisabled : ! ! choice . disabled ,
placeholder : ! ! choice . placeholder ,
customProperties : choice.customProperties ,
2018-05-29 20:55:33 +02:00
} ) ;
2017-11-21 15:10:29 +01:00
}
2019-11-03 18:45:16 +01:00
} ) ;
2017-11-07 15:04:37 +01:00
2019-11-08 10:19:18 +01:00
this . _stopLoading ( ) ;
2017-08-15 17:57:29 +02:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
clearChoices ( ) : this {
2019-03-13 10:05:38 +01:00
this . _store . dispatch ( clearChoices ( ) ) ;
2019-10-29 19:26:11 +01:00
return this ;
2019-03-13 10:05:38 +01:00
}
2019-12-23 19:22:54 +01:00
clearStore ( ) : this {
2018-05-24 10:22:07 +02:00
this . _store . dispatch ( clearAll ( ) ) ;
2019-10-29 19:26:11 +01:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
clearInput ( ) : this {
2018-05-24 10:22:07 +02:00
const shouldSetInputWidth = ! this . _isSelectOneElement ;
2017-08-21 09:53:19 +02:00
this . input . clear ( shouldSetInputWidth ) ;
2017-08-15 17:57:29 +02:00
2018-05-29 16:46:30 +02:00
if ( ! this . _isTextElement && this . _canSearch ) {
2018-05-24 10:22:07 +02:00
this . _isSearching = false ;
this . _store . dispatch ( activateChoices ( true ) ) ;
2016-09-05 23:04:15 +02:00
}
2017-08-15 17:57:29 +02:00
2016-09-05 23:04:15 +02:00
return this ;
}
2019-12-23 19:22:54 +01:00
_render ( ) : void {
2019-01-26 13:36:47 +01:00
if ( this . _store . isLoading ( ) ) {
return ;
}
2018-10-09 13:26:47 +02:00
this . _currentState = this . _store . state ;
const stateChanged =
this . _currentState . choices !== this . _prevState . choices ||
this . _currentState . groups !== this . _prevState . groups ||
this . _currentState . items !== this . _prevState . items ;
const shouldRenderChoices = this . _isSelectElement ;
const shouldRenderItems =
this . _currentState . items !== this . _prevState . items ;
if ( ! stateChanged ) {
return ;
}
if ( shouldRenderChoices ) {
this . _renderChoices ( ) ;
}
if ( shouldRenderItems ) {
this . _renderItems ( ) ;
}
this . _prevState = this . _currentState ;
}
2019-12-23 19:22:54 +01:00
_renderChoices ( ) : void {
2018-10-09 13:26:47 +02:00
const { activeGroups , activeChoices } = this . _store ;
let choiceListFragment = document . createDocumentFragment ( ) ;
this . choiceList . clear ( ) ;
if ( this . config . resetScrollPosition ) {
requestAnimationFrame ( ( ) = > this . choiceList . scrollToTop ( ) ) ;
}
// If we have grouped options
if ( activeGroups . length >= 1 && ! this . _isSearching ) {
// If we have a placeholder choice along with groups
const activePlaceholders = activeChoices . filter (
2021-12-17 22:26:52 +01:00
( activeChoice ) = >
2018-10-09 13:26:47 +02:00
activeChoice . placeholder === true && activeChoice . groupId === - 1 ,
) ;
if ( activePlaceholders . length >= 1 ) {
choiceListFragment = this . _createChoicesFragment (
activePlaceholders ,
choiceListFragment ,
) ;
}
choiceListFragment = this . _createGroupsFragment (
activeGroups ,
activeChoices ,
choiceListFragment ,
) ;
} else if ( activeChoices . length >= 1 ) {
choiceListFragment = this . _createChoicesFragment (
activeChoices ,
choiceListFragment ,
) ;
}
// If we have choices to show
if (
choiceListFragment . childNodes &&
choiceListFragment . childNodes . length > 0
) {
2019-10-21 21:03:57 +02:00
const { activeItems } = this . _store ;
2018-10-09 13:26:47 +02:00
const canAddItem = this . _canAddItem ( activeItems , this . input . value ) ;
// ...and we can select them
if ( canAddItem . response ) {
// ...append them and highlight the first choice
this . choiceList . append ( choiceListFragment ) ;
this . _highlightChoice ( ) ;
} else {
2019-12-23 19:22:54 +01:00
const notice = this . _getTemplate ( 'notice' , canAddItem . notice ) ;
this . choiceList . append ( notice ) ;
2018-10-09 13:26:47 +02:00
}
} else {
// Otherwise show a notice
let dropdownItem ;
let notice ;
if ( this . _isSearching ) {
2019-10-29 22:19:56 +01:00
notice =
typeof this . config . noResultsText === 'function'
? this . config . noResultsText ( )
: this . config . noResultsText ;
2018-10-09 13:26:47 +02:00
dropdownItem = this . _getTemplate ( 'notice' , notice , 'no-results' ) ;
} else {
2019-10-29 22:19:56 +01:00
notice =
typeof this . config . noChoicesText === 'function'
? this . config . noChoicesText ( )
: this . config . noChoicesText ;
2018-10-09 13:26:47 +02:00
dropdownItem = this . _getTemplate ( 'notice' , notice , 'no-choices' ) ;
}
this . choiceList . append ( dropdownItem ) ;
}
}
2019-12-23 19:22:54 +01:00
_renderItems ( ) : void {
2018-10-09 13:26:47 +02:00
const activeItems = this . _store . activeItems || [ ] ;
this . itemList . clear ( ) ;
2018-10-13 16:49:44 +02:00
// Create a fragment to store our list items
// (so we don't have to update the DOM for each item)
const itemListFragment = this . _createItemsFragment ( activeItems ) ;
// If we have items to add, append them
if ( itemListFragment . childNodes ) {
this . itemList . append ( itemListFragment ) ;
2018-10-09 13:26:47 +02:00
}
}
2019-10-25 14:21:38 +02:00
_createGroupsFragment (
2019-12-23 19:22:54 +01:00
groups : Group [ ] ,
choices : Choice [ ] ,
fragment : DocumentFragment = document . createDocumentFragment ( ) ,
) : DocumentFragment {
const getGroupChoices = ( group ) : Choice [ ] = >
2021-12-17 22:26:52 +01:00
choices . filter ( ( choice ) = > {
2018-05-28 14:55:44 +02:00
if ( this . _isSelectOneElement ) {
return choice . groupId === group . id ;
}
2019-10-29 19:26:11 +01:00
2018-05-28 14:55:44 +02:00
return (
choice . groupId === group . id &&
( this . config . renderSelectedChoices === 'always' || ! choice . selected )
) ;
} ) ;
2018-05-23 14:09:45 +02:00
// If sorting is enabled, filter groups
if ( this . config . shouldSort ) {
2019-11-03 14:18:16 +01:00
groups . sort ( this . config . sorter ) ;
2018-05-23 14:09:45 +02:00
}
2021-12-17 22:26:52 +01:00
groups . forEach ( ( group ) = > {
2018-05-23 14:09:45 +02:00
const groupChoices = getGroupChoices ( group ) ;
if ( groupChoices . length >= 1 ) {
const dropdownGroup = this . _getTemplate ( 'choiceGroup' , group ) ;
2019-10-25 14:21:38 +02:00
fragment . appendChild ( dropdownGroup ) ;
this . _createChoicesFragment ( groupChoices , fragment , true ) ;
2018-05-23 14:09:45 +02:00
}
} ) ;
2019-10-25 14:21:38 +02:00
return fragment ;
2018-05-23 14:09:45 +02:00
}
2019-10-25 14:21:38 +02:00
_createChoicesFragment (
2019-12-23 19:22:54 +01:00
choices : Choice [ ] ,
fragment : DocumentFragment = document . createDocumentFragment ( ) ,
2019-10-25 14:21:38 +02:00
withinGroup = false ,
2019-12-23 19:22:54 +01:00
) : DocumentFragment {
2018-05-23 14:09:45 +02:00
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
2021-12-17 22:26:52 +01:00
const { renderSelectedChoices , searchResultLimit , renderChoiceLimit } =
this . config ;
2019-11-03 14:18:16 +01:00
const filter = this . _isSearching ? sortByScore : this.config.sorter ;
2019-12-23 19:22:54 +01:00
const appendChoice = ( choice : Choice ) : void = > {
2018-05-28 14:55:44 +02:00
const shouldRender =
renderSelectedChoices === 'auto'
? this . _isSelectOneElement || ! choice . selected
: true ;
2019-12-23 19:22:54 +01:00
2018-05-23 14:09:45 +02:00
if ( shouldRender ) {
2018-05-28 14:55:44 +02:00
const dropdownItem = this . _getTemplate (
'choice' ,
choice ,
this . config . itemSelectText ,
) ;
2019-12-23 19:22:54 +01:00
2019-10-25 14:21:38 +02:00
fragment . appendChild ( dropdownItem ) ;
2018-05-23 14:09:45 +02:00
}
} ;
let rendererableChoices = choices ;
2018-05-24 10:22:07 +02:00
if ( renderSelectedChoices === 'auto' && ! this . _isSelectOneElement ) {
2021-12-17 22:26:52 +01:00
rendererableChoices = choices . filter ( ( choice ) = > ! choice . selected ) ;
2018-05-23 14:09:45 +02:00
}
// Split array into placeholders and "normal" choices
2018-05-28 14:55:44 +02:00
const { placeholderChoices , normalChoices } = rendererableChoices . reduce (
2019-12-23 19:22:54 +01:00
( acc , choice : Choice ) = > {
2018-05-28 14:55:44 +02:00
if ( choice . placeholder ) {
acc . placeholderChoices . push ( choice ) ;
} else {
acc . normalChoices . push ( choice ) ;
}
2019-10-29 19:26:11 +01:00
2018-05-28 14:55:44 +02:00
return acc ;
} ,
2019-12-23 19:22:54 +01:00
{
placeholderChoices : [ ] as Choice [ ] ,
normalChoices : [ ] as Choice [ ] ,
} ,
2018-05-28 14:55:44 +02:00
) ;
2018-05-23 14:09:45 +02:00
// If sorting is enabled or the user is searching, filter choices
2018-05-24 10:22:07 +02:00
if ( this . config . shouldSort || this . _isSearching ) {
2018-05-23 14:09:45 +02:00
normalChoices . sort ( filter ) ;
}
let choiceLimit = rendererableChoices . length ;
// Prepend placeholeder
2019-11-02 14:49:33 +01:00
const sortedChoices = this . _isSelectOneElement
? [ . . . placeholderChoices , . . . normalChoices ]
: normalChoices ;
2018-05-23 14:09:45 +02:00
2018-05-24 10:22:07 +02:00
if ( this . _isSearching ) {
2018-05-23 14:09:45 +02:00
choiceLimit = searchResultLimit ;
2019-11-03 14:18:16 +01:00
} else if ( renderChoiceLimit && renderChoiceLimit > 0 && ! withinGroup ) {
2018-05-23 14:09:45 +02:00
choiceLimit = renderChoiceLimit ;
}
// Add each choice to dropdown within range
for ( let i = 0 ; i < choiceLimit ; i += 1 ) {
if ( sortedChoices [ i ] ) {
appendChoice ( sortedChoices [ i ] ) ;
}
}
2019-10-25 14:21:38 +02:00
return fragment ;
2018-05-23 14:09:45 +02:00
}
2019-12-23 19:22:54 +01:00
_createItemsFragment (
items : Item [ ] ,
fragment : DocumentFragment = document . createDocumentFragment ( ) ,
) : DocumentFragment {
2018-05-23 14:09:45 +02:00
// Create fragment to add elements to
2019-11-03 14:18:16 +01:00
const { shouldSortItems , sorter , removeItemButton } = this . config ;
2018-05-23 14:09:45 +02:00
// If sorting is enabled, filter items
2018-05-24 10:22:07 +02:00
if ( shouldSortItems && ! this . _isSelectOneElement ) {
2019-11-03 14:18:16 +01:00
items . sort ( sorter ) ;
2018-05-23 14:09:45 +02:00
}
2018-05-24 10:22:07 +02:00
if ( this . _isTextElement ) {
2018-05-23 14:09:45 +02:00
// Update the value of the hidden input
2019-12-23 19:22:54 +01:00
this . passedElement . value = items
. map ( ( { value } ) = > value )
. join ( this . config . delimiter ) ;
2018-05-23 14:09:45 +02:00
} else {
// Update the options of the hidden input
2019-12-23 19:22:54 +01:00
( this . passedElement as WrappedSelect ) . options = items ;
2018-05-23 14:09:45 +02:00
}
2019-12-23 19:22:54 +01:00
const addItemToFragment = ( item : Item ) : void = > {
2018-05-23 14:09:45 +02:00
// Create new list element
2018-05-24 10:22:07 +02:00
const listItem = this . _getTemplate ( 'item' , item , removeItemButton ) ;
2018-05-23 14:09:45 +02:00
// Append it to list
2019-10-25 14:21:38 +02:00
fragment . appendChild ( listItem ) ;
2018-05-23 14:09:45 +02:00
} ;
// Add each list item to list
2019-11-03 14:18:16 +01:00
items . forEach ( addItemToFragment ) ;
2018-05-23 14:09:45 +02:00
2019-10-25 14:21:38 +02:00
return fragment ;
2018-05-23 14:09:45 +02:00
}
2019-12-23 19:22:54 +01:00
_triggerChange ( value ) : void {
2018-03-08 10:20:28 +01:00
if ( value === undefined || value === null ) {
2017-05-18 10:36:33 +02:00
return ;
}
2016-09-05 23:04:15 +02:00
2017-10-18 10:08:27 +02:00
this . passedElement . triggerEvent ( EVENTS . change , {
2017-08-15 10:29:42 +02:00
value ,
2017-01-01 16:32:09 +01:00
} ) ;
2016-09-05 23:04:15 +02:00
}
2016-06-08 15:45:29 +02:00
2019-12-23 19:22:54 +01:00
_selectPlaceholderChoice ( placeholderChoice : Choice ) : void {
this . _addItem ( {
value : placeholderChoice.value ,
label : placeholderChoice.label ,
choiceId : placeholderChoice.id ,
groupId : placeholderChoice.groupId ,
placeholder : placeholderChoice.placeholder ,
} ) ;
2018-05-29 20:55:33 +02:00
2019-12-23 19:22:54 +01:00
this . _triggerChange ( placeholderChoice . value ) ;
2017-10-29 19:56:24 +01:00
}
2019-12-23 19:22:54 +01:00
_handleButtonAction ( activeItems? : Item [ ] , element? : HTMLElement ) : void {
2017-08-30 14:04:19 +02:00
if (
! activeItems ||
! element ||
! this . config . removeItems ||
! this . config . removeItemButton
) {
2017-05-18 10:36:33 +02:00
return ;
}
2016-08-02 22:02:52 +02:00
2019-12-23 19:22:54 +01:00
const itemId =
element . parentNode && ( element . parentNode as HTMLElement ) . dataset . id ;
const itemToRemove =
2021-12-17 22:26:52 +01:00
itemId && activeItems . find ( ( item ) = > item . id === parseInt ( itemId , 10 ) ) ;
2019-12-23 19:22:54 +01:00
if ( ! itemToRemove ) {
return ;
}
2016-06-08 15:45:29 +02:00
2018-05-24 10:22:07 +02:00
// Remove item associated with button
2017-08-30 14:04:19 +02:00
this . _removeItem ( itemToRemove ) ;
this . _triggerChange ( itemToRemove . value ) ;
2016-06-08 15:45:29 +02:00
2019-12-23 19:22:54 +01:00
if ( this . _isSelectOneElement && this . _store . placeholderChoice ) {
this . _selectPlaceholderChoice ( this . _store . placeholderChoice ) ;
2016-06-08 15:45:29 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_handleItemAction (
activeItems? : Item [ ] ,
element? : HTMLElement ,
hasShiftKey = false ,
) : void {
2017-08-30 14:04:19 +02:00
if (
! activeItems ||
! element ||
! this . config . removeItems ||
2018-05-24 10:22:07 +02:00
this . _isSelectOneElement
2017-08-30 14:04:19 +02:00
) {
2017-05-18 10:36:33 +02:00
return ;
}
2016-09-05 23:04:15 +02:00
2019-12-23 19:22:54 +01:00
const passedId = element . dataset . id ;
2016-09-05 23:04:15 +02:00
2017-08-30 14:04:19 +02:00
// We only want to select one item with a click
// so we deselect any items that aren't the target
// unless shift is being pressed
2021-12-17 22:26:52 +01:00
activeItems . forEach ( ( item ) = > {
2019-12-23 19:22:54 +01:00
if ( item . id === parseInt ( ` ${ passedId } ` , 10 ) && ! item . highlighted ) {
2017-08-30 14:04:19 +02:00
this . highlightItem ( item ) ;
} else if ( ! hasShiftKey && item . highlighted ) {
this . unhighlightItem ( item ) ;
}
} ) ;
2016-06-08 15:45:29 +02:00
2017-08-30 14:04:19 +02:00
// Focus input as without focus, a user cannot do anything with a
// highlighted item
this . input . focus ( ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_handleChoiceAction ( activeItems? : Item [ ] , element? : HTMLElement ) : void {
2017-05-18 10:36:33 +02:00
if ( ! activeItems || ! element ) {
return ;
}
2016-09-05 23:04:15 +02:00
// If we are clicking on an option
2019-10-25 14:09:27 +02:00
const { id } = element . dataset ;
2019-12-23 19:22:54 +01:00
const choice = id && this . _store . getChoiceById ( id ) ;
2019-10-29 19:26:11 +01:00
if ( ! choice ) {
return ;
}
2019-12-23 19:22:54 +01:00
2018-05-28 14:55:44 +02:00
const passedKeyCode =
2019-12-23 19:22:54 +01:00
activeItems [ 0 ] && activeItems [ 0 ] . keyCode
? activeItems [ 0 ] . keyCode
: undefined ;
2017-08-16 14:01:17 +02:00
const hasActiveDropdown = this . dropdown . isActive ;
2016-09-05 23:04:15 +02:00
2017-07-19 19:48:46 +02:00
// Update choice keyCode
2017-07-20 13:05:56 +02:00
choice . keyCode = passedKeyCode ;
2017-07-19 19:48:46 +02:00
2017-10-18 10:08:27 +02:00
this . passedElement . triggerEvent ( EVENTS . choice , {
2017-03-28 15:41:12 +02:00
choice ,
} ) ;
2019-10-25 14:09:27 +02:00
if ( ! choice . selected && ! choice . disabled ) {
2016-09-05 23:04:15 +02:00
const canAddItem = this . _canAddItem ( activeItems , choice . value ) ;
if ( canAddItem . response ) {
2018-05-29 20:55:33 +02:00
this . _addItem ( {
value : choice.value ,
label : choice.label ,
choiceId : choice.id ,
groupId : choice.groupId ,
customProperties : choice.customProperties ,
placeholder : choice.placeholder ,
keyCode : choice.keyCode ,
} ) ;
2016-09-05 23:04:15 +02:00
this . _triggerChange ( choice . value ) ;
}
2016-08-02 08:45:08 +02:00
}
2017-07-13 16:59:33 +02:00
this . clearInput ( ) ;
2016-09-04 14:44:31 +02:00
2019-10-25 14:09:27 +02:00
// We want to close the dropdown if we are dealing with a single select box
2018-05-24 10:22:07 +02:00
if ( hasActiveDropdown && this . _isSelectOneElement ) {
2018-05-29 16:08:43 +02:00
this . hideDropdown ( true ) ;
2017-08-27 14:49:35 +02:00
this . containerOuter . focus ( ) ;
2016-07-31 21:02:46 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_handleBackspace ( activeItems? : Item [ ] ) : void {
2017-08-30 14:04:19 +02:00
if ( ! this . config . removeItems || ! activeItems ) {
return ;
}
2016-09-05 23:04:15 +02:00
2017-08-30 14:04:19 +02:00
const lastItem = activeItems [ activeItems . length - 1 ] ;
2021-12-17 22:26:52 +01:00
const hasHighlightedItems = activeItems . some ( ( item ) = > item . highlighted ) ;
2017-08-30 14:04:19 +02:00
// If editing the last item is allowed and there are not other selected items,
// we can edit the item value. Otherwise if we can remove items, remove all selected items
if ( this . config . editItems && ! hasHighlightedItems && lastItem ) {
2018-04-24 13:54:45 +02:00
this . input . value = lastItem . value ;
2017-08-30 14:04:19 +02:00
this . input . setWidth ( ) ;
this . _removeItem ( lastItem ) ;
this . _triggerChange ( lastItem . value ) ;
} else {
if ( ! hasHighlightedItems ) {
2017-10-19 13:35:26 +02:00
// Highlight last item if none already highlighted
2017-08-30 14:04:19 +02:00
this . highlightItem ( lastItem , false ) ;
2016-09-05 23:04:15 +02:00
}
2017-08-30 14:04:19 +02:00
this . removeHighlightedItems ( true ) ;
2016-07-31 22:05:17 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_startLoading ( ) : void {
2019-11-08 10:19:18 +01:00
this . _store . dispatch ( setIsLoading ( true ) ) ;
}
2019-12-23 19:22:54 +01:00
_stopLoading ( ) : void {
2019-11-08 10:19:18 +01:00
this . _store . dispatch ( setIsLoading ( false ) ) ;
2019-01-26 13:36:47 +01:00
}
2019-12-23 19:22:54 +01:00
_handleLoadingState ( setLoading = true ) : void {
2018-05-28 14:55:44 +02:00
let placeholderItem = this . itemList . getChild (
` . ${ this . config . classNames . placeholder } ` ,
) ;
2018-11-03 14:24:52 +01:00
2019-01-26 13:36:47 +01:00
if ( setLoading ) {
2018-11-03 14:24:52 +01:00
this . disable ( ) ;
2017-10-29 19:56:24 +01:00
this . containerOuter . addLoadingState ( ) ;
2018-11-03 14:24:52 +01:00
2018-05-24 10:22:07 +02:00
if ( this . _isSelectOneElement ) {
2017-10-29 19:56:24 +01:00
if ( ! placeholderItem ) {
2018-05-28 14:55:44 +02:00
placeholderItem = this . _getTemplate (
'placeholder' ,
this . config . loadingText ,
) ;
2019-12-23 19:22:54 +01:00
if ( placeholderItem ) {
this . itemList . append ( placeholderItem ) ;
}
2017-10-29 19:56:24 +01:00
} else {
placeholderItem . innerHTML = this . config . loadingText ;
}
} else {
2018-04-24 13:54:45 +02:00
this . input . placeholder = this . config . loadingText ;
2017-10-29 19:56:24 +01:00
}
} else {
2018-11-03 14:24:52 +01:00
this . enable ( ) ;
2017-10-29 19:56:24 +01:00
this . containerOuter . removeLoadingState ( ) ;
2018-05-24 10:22:07 +02:00
if ( this . _isSelectOneElement ) {
2019-12-23 19:22:54 +01:00
if ( placeholderItem ) {
placeholderItem . innerHTML = this . _placeholderValue || '' ;
}
2017-10-29 19:56:24 +01:00
} else {
2018-05-28 14:55:44 +02:00
this . input . placeholder = this . _placeholderValue || '' ;
2017-10-29 19:56:24 +01:00
}
}
}
2019-12-23 19:22:54 +01:00
_handleSearch ( value : string ) : void {
2018-10-09 13:26:47 +02:00
if ( ! value || ! this . input . isFocussed ) {
return ;
}
2019-10-21 21:03:57 +02:00
const { choices } = this . _store ;
2018-10-09 13:26:47 +02:00
const { searchFloor , searchChoices } = this . config ;
2021-12-17 22:26:52 +01:00
const hasUnactiveChoices = choices . some ( ( option ) = > ! option . active ) ;
2018-10-09 13:26:47 +02:00
// Check that we have a value to search and the input was an alphanumeric character
if ( value && value . length >= searchFloor ) {
const resultCount = searchChoices ? this . _searchChoices ( value ) : 0 ;
// Trigger search event
this . passedElement . triggerEvent ( EVENTS . search , {
value ,
resultCount ,
} ) ;
} else if ( hasUnactiveChoices ) {
// Otherwise reset choices to active
this . _isSearching = false ;
this . _store . dispatch ( activateChoices ( true ) ) ;
}
}
2019-12-23 19:22:54 +01:00
_canAddItem ( activeItems : Item [ ] , value : string ) : Notice {
2016-09-05 23:04:15 +02:00
let canAddItem = true ;
2019-10-29 22:19:56 +01:00
let notice =
typeof this . config . addItemText === 'function'
? this . config . addItemText ( value )
: this . config . addItemText ;
2016-09-05 23:04:15 +02:00
2018-05-27 18:22:58 +02:00
if ( ! this . _isSelectOneElement ) {
2018-05-29 10:30:05 +02:00
const isDuplicateValue = existsInArray ( activeItems , value ) ;
2016-09-05 23:04:15 +02:00
2018-05-28 14:55:44 +02:00
if (
this . config . maxItemCount > 0 &&
this . config . maxItemCount <= activeItems . length
) {
2016-09-05 23:04:15 +02:00
// If there is a max entry limit and we have reached that limit
// don't update
canAddItem = false ;
2019-10-29 22:19:56 +01:00
notice =
typeof this . config . maxItemText === 'function'
? this . config . maxItemText ( this . config . maxItemCount )
: this . config . maxItemText ;
2016-09-05 23:04:15 +02:00
}
2016-06-08 15:45:29 +02:00
2018-05-29 10:30:05 +02:00
if (
! this . config . duplicateItemsAllowed &&
isDuplicateValue &&
canAddItem
) {
2018-05-27 18:22:58 +02:00
canAddItem = false ;
2019-10-29 22:19:56 +01:00
notice =
typeof this . config . uniqueItemText === 'function'
? this . config . uniqueItemText ( value )
: this . config . uniqueItemText ;
2018-05-27 18:22:58 +02:00
}
2019-02-11 23:56:21 +01:00
if (
this . _isTextElement &&
this . config . addItems &&
2019-02-12 19:35:46 +01:00
canAddItem &&
2019-10-29 18:29:31 +01:00
typeof this . config . addItemFilter === 'function' &&
! this . config . addItemFilter ( value )
2019-02-11 23:56:21 +01:00
) {
canAddItem = false ;
2019-10-29 18:29:31 +01:00
notice =
typeof this . config . customAddItemText === 'function'
? this . config . customAddItemText ( value )
: this . config . customAddItemText ;
2019-02-11 23:56:21 +01:00
}
2016-06-08 15:45:29 +02:00
}
2016-09-05 23:04:15 +02:00
return {
response : canAddItem ,
notice ,
} ;
}
2019-12-23 19:22:54 +01:00
_searchChoices ( value : string ) : number {
2019-10-29 22:19:56 +01:00
const newValue = typeof value === 'string' ? value . trim ( ) : value ;
const currentValue =
typeof this . _currentValue === 'string'
? this . _currentValue . trim ( )
: this . _currentValue ;
2016-09-27 14:44:35 +02:00
2017-10-19 13:35:26 +02:00
if ( newValue . length < 1 && newValue === ` ${ currentValue } ` ) {
return 0 ;
2016-09-27 14:44:35 +02:00
}
2017-07-31 17:17:03 +02:00
2017-10-19 13:35:26 +02:00
// If new value matches the desired length and is not the same as the current value with a space
2018-05-24 10:22:07 +02:00
const haystack = this . _store . searchableChoices ;
2017-10-19 13:35:26 +02:00
const needle = newValue ;
2018-05-29 21:30:58 +02:00
const keys = [ . . . this . config . searchFields ] ;
2019-12-23 19:22:54 +01:00
const options = Object . assign ( this . config . fuseOptions , {
keys ,
includeMatches : true ,
} ) ;
2017-10-19 13:35:26 +02:00
const fuse = new Fuse ( haystack , options ) ;
2019-12-23 19:22:54 +01:00
const results : Result < Choice > [ ] = fuse . search ( needle ) as any [ ] ; // see https://github.com/krisk/Fuse/issues/303
2017-10-19 13:35:26 +02:00
2018-05-24 10:22:07 +02:00
this . _currentValue = newValue ;
this . _highlightPosition = 0 ;
this . _isSearching = true ;
this . _store . dispatch ( filterChoices ( results ) ) ;
2017-10-19 13:35:26 +02:00
return results . length ;
2016-09-27 14:44:35 +02:00
}
2019-12-23 19:22:54 +01:00
_addEventListeners ( ) : void {
2019-10-29 18:46:10 +01:00
const { documentElement } = document ;
// capture events - can cancel event processing or propagation
documentElement . addEventListener ( 'touchend' , this . _onTouchEnd , true ) ;
2019-11-12 10:47:41 +01:00
this . containerOuter . element . addEventListener (
'keydown' ,
this . _onKeyDown ,
true ,
) ;
this . containerOuter . element . addEventListener (
'mousedown' ,
this . _onMouseDown ,
true ,
) ;
2019-10-29 18:46:10 +01:00
// passive events - doesn't call `preventDefault` or `stopPropagation`
documentElement . addEventListener ( 'click' , this . _onClick , { passive : true } ) ;
documentElement . addEventListener ( 'touchmove' , this . _onTouchMove , {
passive : true ,
} ) ;
2019-11-12 10:47:41 +01:00
this . dropdown . element . addEventListener ( 'mouseover' , this . _onMouseOver , {
2019-10-29 18:46:10 +01:00
passive : true ,
} ) ;
2016-09-05 23:04:15 +02:00
2018-05-24 10:22:07 +02:00
if ( this . _isSelectOneElement ) {
2019-10-29 18:46:10 +01:00
this . containerOuter . element . addEventListener ( 'focus' , this . _onFocus , {
passive : true ,
} ) ;
this . containerOuter . element . addEventListener ( 'blur' , this . _onBlur , {
passive : true ,
} ) ;
2016-06-08 15:45:29 +02:00
}
2019-10-29 18:46:10 +01:00
this . input . element . addEventListener ( 'keyup' , this . _onKeyUp , {
passive : true ,
} ) ;
this . input . element . addEventListener ( 'focus' , this . _onFocus , {
passive : true ,
} ) ;
this . input . element . addEventListener ( 'blur' , this . _onBlur , {
passive : true ,
} ) ;
2017-08-23 14:23:37 +02:00
2018-05-28 17:22:22 +02:00
if ( this . input . element . form ) {
2019-10-29 18:46:10 +01:00
this . input . element . form . addEventListener ( 'reset' , this . _onFormReset , {
passive : true ,
} ) ;
2018-05-28 17:22:22 +02:00
}
2017-08-23 14:23:37 +02:00
this . input . addEventListeners ( ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_removeEventListeners ( ) : void {
2019-10-29 18:46:10 +01:00
const { documentElement } = document ;
documentElement . removeEventListener ( 'touchend' , this . _onTouchEnd , true ) ;
2019-11-12 10:47:41 +01:00
this . containerOuter . element . removeEventListener (
'keydown' ,
this . _onKeyDown ,
true ,
) ;
this . containerOuter . element . removeEventListener (
'mousedown' ,
this . _onMouseDown ,
true ,
) ;
2019-10-29 18:46:10 +01:00
2019-11-08 10:19:18 +01:00
documentElement . removeEventListener ( 'click' , this . _onClick ) ;
documentElement . removeEventListener ( 'touchmove' , this . _onTouchMove ) ;
2019-11-12 10:47:41 +01:00
this . dropdown . element . removeEventListener ( 'mouseover' , this . _onMouseOver ) ;
2016-09-05 23:04:15 +02:00
2018-05-24 10:22:07 +02:00
if ( this . _isSelectOneElement ) {
2019-11-08 10:19:18 +01:00
this . containerOuter . element . removeEventListener ( 'focus' , this . _onFocus ) ;
this . containerOuter . element . removeEventListener ( 'blur' , this . _onBlur ) ;
2016-07-31 21:02:46 +02:00
}
2019-11-12 10:47:41 +01:00
this . input . element . removeEventListener ( 'keyup' , this . _onKeyUp ) ;
2019-11-08 10:19:18 +01:00
this . input . element . removeEventListener ( 'focus' , this . _onFocus ) ;
this . input . element . removeEventListener ( 'blur' , this . _onBlur ) ;
2018-05-28 17:22:22 +02:00
if ( this . input . element . form ) {
2019-11-08 10:19:18 +01:00
this . input . element . form . removeEventListener ( 'reset' , this . _onFormReset ) ;
2018-05-28 17:22:22 +02:00
}
2017-08-23 14:23:37 +02:00
this . input . removeEventListeners ( ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_onKeyDown ( event : KeyboardEvent ) : void {
2019-11-19 19:34:08 +01:00
const { keyCode } = event ;
2019-10-21 21:03:57 +02:00
const { activeItems } = this . _store ;
2017-08-27 14:49:35 +02:00
const hasFocusedInput = this . input . isFocussed ;
2017-08-16 14:01:17 +02:00
const hasActiveDropdown = this . dropdown . isActive ;
2019-10-29 22:19:56 +01:00
const hasItems = this . itemList . hasChildren ( ) ;
2018-05-27 12:57:21 +02:00
const keyString = String . fromCharCode ( keyCode ) ;
2019-11-22 20:09:45 +01:00
const wasAlphaNumericChar = /[a-zA-Z0-9-_ ]/ . test ( keyString ) ;
2018-10-09 14:17:11 +02:00
const {
BACK_KEY ,
DELETE_KEY ,
ENTER_KEY ,
A_KEY ,
ESC_KEY ,
UP_KEY ,
DOWN_KEY ,
PAGE_UP_KEY ,
PAGE_DOWN_KEY ,
} = KEY_CODES ;
2016-09-05 23:04:15 +02:00
2019-11-22 20:09:45 +01:00
if ( ! this . _isTextElement && ! hasActiveDropdown && wasAlphaNumericChar ) {
2018-05-29 16:08:43 +02:00
this . showDropdown ( ) ;
2019-11-22 20:09:45 +01:00
if ( ! this . input . isFocussed ) {
/ *
We update the input value with the pressed key as
the input was not focussed at the time of key press
therefore does not have the value of the key .
* /
this . input . value += keyString . toLowerCase ( ) ;
}
2016-08-14 17:10:53 +02:00
}
2019-12-23 19:22:54 +01:00
switch ( keyCode ) {
case A_KEY :
return this . _onSelectKey ( event , hasItems ) ;
case ENTER_KEY :
return this . _onEnterKey ( event , activeItems , hasActiveDropdown ) ;
case ESC_KEY :
return this . _onEscapeKey ( hasActiveDropdown ) ;
case UP_KEY :
case PAGE_UP_KEY :
case DOWN_KEY :
case PAGE_DOWN_KEY :
return this . _onDirectionKey ( event , hasActiveDropdown ) ;
case DELETE_KEY :
case BACK_KEY :
return this . _onDeleteKey ( event , activeItems , hasFocusedInput ) ;
default :
}
}
_onKeyUp ( {
target ,
keyCode ,
} : Pick < KeyboardEvent , ' target ' | ' keyCode ' > ) : void {
2019-10-21 21:03:57 +02:00
const { value } = this . input ;
const { activeItems } = this . _store ;
2017-05-11 16:11:26 +02:00
const canAddItem = this . _canAddItem ( activeItems , value ) ;
2019-02-12 19:35:46 +01:00
const { BACK_KEY : backKey , DELETE_KEY : deleteKey } = KEY_CODES ;
2017-05-11 16:11:26 +02:00
2016-09-05 23:04:15 +02:00
// We are typing into a text input and have a value, we want to show a dropdown
// notice. Otherwise hide the dropdown
2018-05-24 10:22:07 +02:00
if ( this . _isTextElement ) {
2019-02-12 19:35:46 +01:00
const canShowDropdownNotice = canAddItem . notice && value ;
2019-11-22 20:09:45 +01:00
2019-02-12 19:35:46 +01:00
if ( canShowDropdownNotice ) {
const dropdownItem = this . _getTemplate ( 'notice' , canAddItem . notice ) ;
this . dropdown . element . innerHTML = dropdownItem . outerHTML ;
this . showDropdown ( true ) ;
2017-08-30 14:04:19 +02:00
} else {
2018-05-29 16:08:43 +02:00
this . hideDropdown ( true ) ;
2016-09-05 23:04:15 +02:00
}
} else {
2019-11-22 20:09:45 +01:00
const wasRemovalKeyCode = keyCode === backKey || keyCode === deleteKey ;
2019-12-23 19:22:54 +01:00
const userHasRemovedValue =
wasRemovalKeyCode && target && ! ( target as HTMLSelectElement ) . value ;
2019-02-12 19:35:46 +01:00
const canReactivateChoices = ! this . _isTextElement && this . _isSearching ;
const canSearch = this . _canSearch && canAddItem . response ;
if ( userHasRemovedValue && canReactivateChoices ) {
this . _isSearching = false ;
this . _store . dispatch ( activateChoices ( true ) ) ;
} else if ( canSearch ) {
2018-04-24 13:54:45 +02:00
this . _handleSearch ( this . input . value ) ;
2016-09-05 23:04:15 +02:00
}
2016-03-17 16:00:22 +01:00
}
2018-05-29 16:46:30 +02:00
this . _canSearch = this . config . searchEnabled ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_onSelectKey ( event : KeyboardEvent , hasItems : boolean ) : void {
2019-11-19 19:34:08 +01:00
const { ctrlKey , metaKey } = event ;
const hasCtrlDownKeyPressed = ctrlKey || metaKey ;
2019-11-22 20:09:45 +01:00
2018-10-09 14:17:11 +02:00
// If CTRL + A or CMD + A have been pressed and there are items to select
if ( hasCtrlDownKeyPressed && hasItems ) {
this . _canSearch = false ;
2019-02-12 19:35:46 +01:00
const shouldHightlightAll =
2018-10-09 14:17:11 +02:00
this . config . removeItems &&
! this . input . value &&
2019-02-12 19:35:46 +01:00
this . input . element === document . activeElement ;
if ( shouldHightlightAll ) {
2018-10-09 14:17:11 +02:00
this . highlightAll ( ) ;
}
}
}
2019-12-23 19:22:54 +01:00
_onEnterKey (
event : KeyboardEvent ,
activeItems : Item [ ] ,
hasActiveDropdown : boolean ,
) : void {
2019-11-19 19:34:08 +01:00
const { target } = event ;
2018-10-09 14:17:11 +02:00
const { ENTER_KEY : enterKey } = KEY_CODES ;
2019-12-23 19:22:54 +01:00
const targetWasButton =
target && ( target as HTMLElement ) . hasAttribute ( 'data-button' ) ;
2019-02-12 19:35:46 +01:00
2019-12-23 19:22:54 +01:00
if ( this . _isTextElement && target && ( target as HTMLInputElement ) . value ) {
2019-10-21 21:03:57 +02:00
const { value } = this . input ;
2018-10-09 14:17:11 +02:00
const canAddItem = this . _canAddItem ( activeItems , value ) ;
if ( canAddItem . response ) {
this . hideDropdown ( true ) ;
this . _addItem ( { value } ) ;
this . _triggerChange ( value ) ;
this . clearInput ( ) ;
}
}
2019-02-12 19:35:46 +01:00
if ( targetWasButton ) {
2019-12-23 19:22:54 +01:00
this . _handleButtonAction ( activeItems , target as HTMLElement ) ;
2018-10-09 14:17:11 +02:00
event . preventDefault ( ) ;
}
if ( hasActiveDropdown ) {
2019-02-12 19:35:46 +01:00
const highlightedChoice = this . dropdown . getChild (
2018-10-09 14:17:11 +02:00
` . ${ this . config . classNames . highlightedState } ` ,
) ;
2019-02-12 19:35:46 +01:00
if ( highlightedChoice ) {
2018-10-09 14:17:11 +02:00
// add enter keyCode value
if ( activeItems [ 0 ] ) {
activeItems [ 0 ] . keyCode = enterKey ; // eslint-disable-line no-param-reassign
}
2019-02-12 19:35:46 +01:00
this . _handleChoiceAction ( activeItems , highlightedChoice ) ;
2018-10-09 14:17:11 +02:00
}
2018-10-30 20:20:16 +01:00
event . preventDefault ( ) ;
2018-10-09 14:17:11 +02:00
} else if ( this . _isSelectOneElement ) {
this . showDropdown ( ) ;
event . preventDefault ( ) ;
}
}
2019-12-23 19:22:54 +01:00
_onEscapeKey ( hasActiveDropdown : boolean ) : void {
2018-10-09 14:17:11 +02:00
if ( hasActiveDropdown ) {
this . hideDropdown ( true ) ;
this . containerOuter . focus ( ) ;
}
}
2019-12-23 19:22:54 +01:00
_onDirectionKey ( event : KeyboardEvent , hasActiveDropdown : boolean ) : void {
2019-11-19 19:34:08 +01:00
const { keyCode , metaKey } = event ;
2018-10-09 14:17:11 +02:00
const {
DOWN_KEY : downKey ,
PAGE_UP_KEY : pageUpKey ,
PAGE_DOWN_KEY : pageDownKey ,
} = KEY_CODES ;
// If up or down key is pressed, traverse through options
if ( hasActiveDropdown || this . _isSelectOneElement ) {
this . showDropdown ( ) ;
this . _canSearch = false ;
const directionInt =
keyCode === downKey || keyCode === pageDownKey ? 1 : - 1 ;
const skipKey =
metaKey || keyCode === pageDownKey || keyCode === pageUpKey ;
const selectableChoiceIdentifier = '[data-choice-selectable]' ;
let nextEl ;
if ( skipKey ) {
if ( directionInt > 0 ) {
2019-10-30 18:28:15 +01:00
nextEl = this . dropdown . element . querySelector (
` ${ selectableChoiceIdentifier } :last-of-type ` ,
) ;
2018-10-09 14:17:11 +02:00
} else {
nextEl = this . dropdown . element . querySelector (
selectableChoiceIdentifier ,
) ;
}
} else {
const currentEl = this . dropdown . element . querySelector (
` . ${ this . config . classNames . highlightedState } ` ,
) ;
if ( currentEl ) {
nextEl = getAdjacentEl (
currentEl ,
selectableChoiceIdentifier ,
directionInt ,
) ;
} else {
nextEl = this . dropdown . element . querySelector (
selectableChoiceIdentifier ,
) ;
}
}
if ( nextEl ) {
// We prevent default to stop the cursor moving
// when pressing the arrow
if (
! isScrolledIntoView ( nextEl , this . choiceList . element , directionInt )
) {
2019-11-03 14:18:16 +01:00
this . choiceList . scrollToChildElement ( nextEl , directionInt ) ;
2018-10-09 14:17:11 +02:00
}
this . _highlightChoice ( nextEl ) ;
}
// Prevent default to maintain cursor position whilst
// traversing dropdown options
event . preventDefault ( ) ;
}
}
2019-12-23 19:22:54 +01:00
_onDeleteKey (
event : KeyboardEvent ,
activeItems : Item [ ] ,
hasFocusedInput : boolean ,
) : void {
2019-11-19 19:34:08 +01:00
const { target } = event ;
2018-10-09 14:17:11 +02:00
// If backspace or delete key is pressed and the input has no value
2019-12-23 19:22:54 +01:00
if (
! this . _isSelectOneElement &&
! ( target as HTMLInputElement ) . value &&
hasFocusedInput
) {
2018-10-09 14:17:11 +02:00
this . _handleBackspace ( activeItems ) ;
event . preventDefault ( ) ;
}
}
2019-12-23 19:22:54 +01:00
_onTouchMove ( ) : void {
2019-02-12 19:35:46 +01:00
if ( this . _wasTap ) {
2018-05-24 10:22:07 +02:00
this . _wasTap = false ;
2016-09-05 23:04:15 +02:00
}
}
2019-12-23 19:22:54 +01:00
_onTouchEnd ( event : TouchEvent ) : void {
const { target } = event || ( event as TouchEvent ) . touches [ 0 ] ;
2019-02-12 19:35:46 +01:00
const touchWasWithinContainer =
2019-12-23 19:22:54 +01:00
this . _wasTap && this . containerOuter . element . contains ( target as Node ) ;
2018-05-28 15:18:44 +02:00
2019-02-12 19:35:46 +01:00
if ( touchWasWithinContainer ) {
const containerWasExactTarget =
2018-05-28 15:18:44 +02:00
target === this . containerOuter . element ||
target === this . containerInner . element ;
2019-02-12 19:35:46 +01:00
if ( containerWasExactTarget ) {
2018-05-24 10:22:07 +02:00
if ( this . _isTextElement ) {
2017-08-27 14:49:35 +02:00
this . input . focus ( ) ;
2019-02-12 19:35:46 +01:00
} else if ( this . _isSelectMultipleElement ) {
2018-05-29 16:08:43 +02:00
this . showDropdown ( ) ;
2016-05-07 14:30:07 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-02-12 19:35:46 +01:00
2016-09-05 23:04:15 +02:00
// Prevents focus event firing
2018-05-27 12:57:21 +02:00
event . stopPropagation ( ) ;
2016-05-04 10:02:22 +02:00
}
2016-04-22 20:45:50 +02:00
2018-05-24 10:22:07 +02:00
this . _wasTap = true ;
2016-09-05 23:04:15 +02:00
}
2019-11-12 10:47:41 +01:00
/ * *
* Handles mousedown event in capture mode for containetOuter . element
* /
2019-12-23 19:22:54 +01:00
_onMouseDown ( event : MouseEvent ) : void {
2019-11-12 10:47:41 +01:00
const { target } = event ;
if ( ! ( target instanceof HTMLElement ) ) {
return ;
2017-08-02 10:30:00 +02:00
}
2019-11-12 10:47:41 +01:00
// If we have our mouse down on the scrollbar and are on IE11...
if ( IS_IE11 && this . choiceList . element . contains ( target ) ) {
// check if click was on a scrollbar area
2019-12-23 19:22:54 +01:00
const firstChoice = this . choiceList . element
. firstElementChild as HTMLElement ;
2019-11-12 10:47:41 +01:00
const isOnScrollbar =
this . _direction === 'ltr'
? event . offsetX >= firstChoice . offsetWidth
: event . offsetX < firstChoice . offsetLeft ;
this . _isScrollingOnIe = isOnScrollbar ;
}
if ( target === this . input . element ) {
2018-05-29 16:08:43 +02:00
return ;
}
2019-11-12 10:47:41 +01:00
const item = target . closest ( '[data-button],[data-item],[data-choice]' ) ;
if ( item instanceof HTMLElement ) {
const hasShiftKey = event . shiftKey ;
const { activeItems } = this . _store ;
const { dataset } = item ;
if ( 'button' in dataset ) {
this . _handleButtonAction ( activeItems , item ) ;
} else if ( 'item' in dataset ) {
this . _handleItemAction ( activeItems , item , hasShiftKey ) ;
} else if ( 'choice' in dataset ) {
this . _handleChoiceAction ( activeItems , item ) ;
}
2016-05-16 15:46:04 +02:00
}
2019-11-24 18:32:02 +01:00
2018-05-29 16:08:43 +02:00
event . preventDefault ( ) ;
2016-09-05 23:04:15 +02:00
}
2019-11-12 10:47:41 +01:00
/ * *
* Handles mouseover event over this . dropdown
* @param { MouseEvent } event
* /
2019-12-23 19:22:54 +01:00
_onMouseOver ( { target } : Pick < MouseEvent , ' target ' > ) : void {
2019-11-12 10:47:41 +01:00
if ( target instanceof HTMLElement && 'choice' in target . dataset ) {
2018-05-27 12:57:21 +02:00
this . _highlightChoice ( target ) ;
2017-10-29 19:56:24 +01:00
}
}
2019-12-23 19:22:54 +01:00
_onClick ( { target } : Pick < MouseEvent , ' target ' > ) : void {
2019-02-12 19:35:46 +01:00
const clickWasWithinContainer = this . containerOuter . element . contains (
2019-12-23 19:22:54 +01:00
target as Node ,
2019-02-12 19:35:46 +01:00
) ;
if ( clickWasWithinContainer ) {
2018-10-30 21:28:40 +01:00
if ( ! this . dropdown . isActive && ! this . containerOuter . isDisabled ) {
2018-05-24 10:22:07 +02:00
if ( this . _isTextElement ) {
2017-08-21 09:53:19 +02:00
if ( document . activeElement !== this . input . element ) {
2017-08-27 14:49:35 +02:00
this . input . focus ( ) ;
2016-09-05 23:04:15 +02:00
}
} else {
2017-08-15 10:29:42 +02:00
this . showDropdown ( ) ;
2018-05-29 16:08:43 +02:00
this . containerOuter . focus ( ) ;
2016-08-07 23:16:05 +02:00
}
2017-08-15 13:50:37 +02:00
} else if (
2018-05-24 10:22:07 +02:00
this . _isSelectOneElement &&
2017-08-21 09:53:19 +02:00
target !== this . input . element &&
2019-12-23 19:22:54 +01:00
! this . dropdown . element . contains ( target as Node )
2017-08-15 13:50:37 +02:00
) {
2018-05-29 16:08:43 +02:00
this . hideDropdown ( ) ;
2016-09-05 23:04:15 +02:00
}
} else {
2019-10-03 10:41:53 +02:00
const hasHighlightedItems = this . _store . highlightedActiveItems . length > 0 ;
2016-09-05 23:04:15 +02:00
if ( hasHighlightedItems ) {
this . unhighlightAll ( ) ;
}
2017-08-27 14:49:35 +02:00
this . containerOuter . removeFocusState ( ) ;
2018-05-29 16:08:43 +02:00
this . hideDropdown ( true ) ;
2016-08-07 23:09:49 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_onFocus ( { target } : Pick < FocusEvent , ' target ' > ) : void {
const focusWasWithinContainer =
target && this . containerOuter . element . contains ( target as Node ) ;
2019-02-12 19:35:46 +01:00
if ( ! focusWasWithinContainer ) {
2017-10-19 13:35:26 +02:00
return ;
}
const focusActions = {
2019-12-23 19:22:54 +01:00
[ TEXT_TYPE ] : ( ) : void = > {
2017-10-19 13:35:26 +02:00
if ( target === this . input . element ) {
2017-08-27 14:49:35 +02:00
this . containerOuter . addFocusState ( ) ;
2017-10-19 13:35:26 +02:00
}
} ,
2019-12-23 19:22:54 +01:00
[ SELECT_ONE_TYPE ] : ( ) : void = > {
2017-10-19 13:35:26 +02:00
this . containerOuter . addFocusState ( ) ;
if ( target === this . input . element ) {
2018-05-29 16:08:43 +02:00
this . showDropdown ( true ) ;
2017-10-19 13:35:26 +02:00
}
} ,
2019-12-23 19:22:54 +01:00
[ SELECT_MULTIPLE_TYPE ] : ( ) : void = > {
2017-10-19 13:35:26 +02:00
if ( target === this . input . element ) {
2018-05-29 16:08:43 +02:00
this . showDropdown ( true ) ;
2017-10-19 13:35:26 +02:00
// If element is a select box, the focused element is the container and the dropdown
// isn't already open, focus and show dropdown
this . containerOuter . addFocusState ( ) ;
}
} ,
} ;
2016-04-29 19:06:46 +02:00
2017-10-19 13:35:26 +02:00
focusActions [ this . passedElement . element . type ] ( ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_onBlur ( { target } : Pick < FocusEvent , ' target ' > ) : void {
const blurWasWithinContainer =
target && this . containerOuter . element . contains ( target as Node ) ;
2019-02-12 19:35:46 +01:00
if ( blurWasWithinContainer && ! this . _isScrollingOnIe ) {
2019-10-21 21:03:57 +02:00
const { activeItems } = this . _store ;
2021-12-17 22:26:52 +01:00
const hasHighlightedItems = activeItems . some ( ( item ) = > item . highlighted ) ;
2016-09-05 23:04:15 +02:00
const blurActions = {
2019-12-23 19:22:54 +01:00
[ TEXT_TYPE ] : ( ) : void = > {
2017-08-21 09:53:19 +02:00
if ( target === this . input . element ) {
2017-08-27 14:49:35 +02:00
this . containerOuter . removeFocusState ( ) ;
2016-08-14 23:14:37 +02:00
if ( hasHighlightedItems ) {
2016-09-05 23:04:15 +02:00
this . unhighlightAll ( ) ;
}
2018-05-29 16:08:43 +02:00
this . hideDropdown ( true ) ;
2016-09-05 23:04:15 +02:00
}
} ,
2019-12-23 19:22:54 +01:00
[ SELECT_ONE_TYPE ] : ( ) : void = > {
2017-08-27 14:49:35 +02:00
this . containerOuter . removeFocusState ( ) ;
2018-05-28 14:55:44 +02:00
if (
target === this . input . element ||
2018-05-29 16:46:30 +02:00
( target === this . containerOuter . element && ! this . _canSearch )
2018-05-28 14:55:44 +02:00
) {
2018-05-29 16:08:43 +02:00
this . hideDropdown ( true ) ;
2016-09-05 23:04:15 +02:00
}
} ,
2019-12-23 19:22:54 +01:00
[ SELECT_MULTIPLE_TYPE ] : ( ) : void = > {
2017-08-21 09:53:19 +02:00
if ( target === this . input . element ) {
2017-08-27 14:49:35 +02:00
this . containerOuter . removeFocusState ( ) ;
2018-05-29 16:08:43 +02:00
this . hideDropdown ( true ) ;
2016-09-05 23:04:15 +02:00
if ( hasHighlightedItems ) {
this . unhighlightAll ( ) ;
}
}
} ,
} ;
2016-04-14 15:54:47 +02:00
2017-10-13 14:43:58 +02:00
blurActions [ this . passedElement . element . type ] ( ) ;
2017-08-02 10:30:00 +02:00
} else {
2017-08-02 10:42:12 +02:00
// On IE11, clicking the scollbar blurs our input and thus
// closes the dropdown. To stop this, we refocus our input
// if we know we are on IE *and* are scrolling.
2018-05-24 10:22:07 +02:00
this . _isScrollingOnIe = false ;
2017-08-21 09:53:19 +02:00
this . input . element . focus ( ) ;
2016-04-09 12:29:56 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_onFormReset ( ) : void {
2018-05-28 17:22:22 +02:00
this . _store . dispatch ( resetTo ( this . _initialState ) ) ;
}
2019-12-23 19:22:54 +01:00
_highlightChoice ( el : HTMLElement | null = null ) : void {
const choices : HTMLElement [ ] = Array . from (
2018-05-28 14:55:44 +02:00
this . dropdown . element . querySelectorAll ( '[data-choice-selectable]' ) ,
) ;
2016-09-05 23:04:15 +02:00
2017-11-21 16:19:46 +01:00
if ( ! choices . length ) {
return ;
}
2016-09-05 23:04:15 +02:00
2017-11-21 16:19:46 +01:00
let passedEl = el ;
const highlightedChoices = Array . from (
2018-05-28 14:55:44 +02:00
this . dropdown . element . querySelectorAll (
` . ${ this . config . classNames . highlightedState } ` ,
) ,
2017-11-21 16:19:46 +01:00
) ;
2016-09-05 23:04:15 +02:00
2017-11-21 16:19:46 +01:00
// Remove any highlighted choices
2021-12-17 22:26:52 +01:00
highlightedChoices . forEach ( ( choice ) = > {
2017-11-21 16:19:46 +01:00
choice . classList . remove ( this . config . classNames . highlightedState ) ;
choice . setAttribute ( 'aria-selected' , 'false' ) ;
} ) ;
if ( passedEl ) {
2018-05-24 10:22:07 +02:00
this . _highlightPosition = choices . indexOf ( passedEl ) ;
2017-11-21 16:19:46 +01:00
} else {
// Highlight choice based on last known highlight location
2018-05-24 10:22:07 +02:00
if ( choices . length > this . _highlightPosition ) {
2017-11-21 16:19:46 +01:00
// If we have an option to highlight
2018-05-24 10:22:07 +02:00
passedEl = choices [ this . _highlightPosition ] ;
2016-09-05 23:04:15 +02:00
} else {
2017-11-21 16:19:46 +01:00
// Otherwise highlight the option before
passedEl = choices [ choices . length - 1 ] ;
}
2016-04-21 15:42:57 +02:00
2017-11-21 16:19:46 +01:00
if ( ! passedEl ) {
passedEl = choices [ 0 ] ;
2016-09-05 23:04:15 +02:00
}
2017-11-21 16:19:46 +01:00
}
2017-06-02 22:34:07 +02:00
2017-11-21 16:19:46 +01:00
passedEl . classList . add ( this . config . classNames . highlightedState ) ;
passedEl . setAttribute ( 'aria-selected' , 'true' ) ;
2018-06-07 17:54:11 +02:00
this . passedElement . triggerEvent ( EVENTS . highlightChoice , { el : passedEl } ) ;
2017-08-16 18:28:50 +02:00
2018-05-29 16:08:43 +02:00
if ( this . dropdown . isActive ) {
2017-11-21 16:19:46 +01:00
// IE11 ignores aria-label and blocks virtual keyboard
// if aria-activedescendant is set without a dropdown
this . input . setActiveDescendant ( passedEl . id ) ;
this . containerOuter . setActiveDescendant ( passedEl . id ) ;
2016-03-24 15:42:03 +01:00
}
2016-09-05 23:04:15 +02:00
}
2018-05-29 20:55:33 +02:00
_addItem ( {
2017-08-15 13:50:37 +02:00
value ,
label = null ,
choiceId = - 1 ,
groupId = - 1 ,
2019-12-23 19:22:54 +01:00
customProperties = { } ,
2017-08-15 13:50:37 +02:00
placeholder = false ,
2019-12-23 19:22:54 +01:00
keyCode = - 1 ,
} : {
value : string ;
label? : string | null ;
choiceId? : number ;
groupId? : number ;
customProperties? : object ;
placeholder? : boolean ;
keyCode? : number ;
} ) : void {
2019-10-29 22:19:56 +01:00
let passedValue = typeof value === 'string' ? value . trim ( ) : value ;
2018-05-24 10:22:07 +02:00
2019-10-21 21:03:57 +02:00
const { items } = this . _store ;
2016-09-05 23:04:15 +02:00
const passedLabel = label || passedValue ;
2019-11-08 10:19:18 +01:00
const passedOptionId = choiceId || - 1 ;
2018-05-24 10:22:07 +02:00
const group = groupId >= 0 ? this . _store . getGroupById ( groupId ) : null ;
2017-01-01 16:32:09 +01:00
const id = items ? items . length + 1 : 1 ;
2016-09-05 23:04:15 +02:00
// If a prepended value has been passed, prepend it
if ( this . config . prependValue ) {
passedValue = this . config . prependValue + passedValue . toString ( ) ;
2016-04-17 13:02:28 +02:00
}
2016-09-05 23:04:15 +02:00
// If an appended value has been passed, append it
if ( this . config . appendValue ) {
passedValue += this . config . appendValue . toString ( ) ;
2016-03-21 19:53:26 +01:00
}
2016-04-09 12:29:56 +02:00
2018-05-24 10:22:07 +02:00
this . _store . dispatch (
2018-05-29 20:55:33 +02:00
addItem ( {
value : passedValue ,
label : passedLabel ,
2017-07-19 21:47:42 +02:00
id ,
2018-05-29 20:55:33 +02:00
choiceId : passedOptionId ,
2017-07-19 21:47:42 +02:00
groupId ,
customProperties ,
2017-08-02 15:05:26 +02:00
placeholder ,
2019-12-23 19:22:54 +01:00
keyCode ,
2018-05-29 20:55:33 +02:00
} ) ,
2017-07-19 21:47:42 +02:00
) ;
2016-06-22 00:06:23 +02:00
2018-05-24 10:22:07 +02:00
if ( this . _isSelectOneElement ) {
2016-09-05 23:04:15 +02:00
this . removeActiveItems ( id ) ;
2016-06-22 00:06:23 +02:00
}
2017-01-01 16:32:09 +01:00
// Trigger change event
2018-11-25 13:48:49 +01:00
this . passedElement . triggerEvent ( EVENTS . addItem , {
id ,
value : passedValue ,
label : passedLabel ,
2019-12-23 19:22:54 +01:00
customProperties ,
2019-11-24 18:32:02 +01:00
groupValue : group && group . value ? group.value : null ,
2019-12-23 19:22:54 +01:00
keyCode ,
2018-11-25 13:48:49 +01:00
} ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_removeItem ( item : Item ) : void {
2019-11-24 18:32:02 +01:00
const { id , value , label , customProperties , choiceId , groupId } = item ;
2019-12-23 19:22:54 +01:00
const group =
groupId && groupId >= 0 ? this . _store . getGroupById ( groupId ) : null ;
if ( ! id || ! choiceId ) {
return ;
}
2016-06-22 00:06:23 +02:00
2018-05-24 10:22:07 +02:00
this . _store . dispatch ( removeItem ( id , choiceId ) ) ;
2019-11-24 18:32:02 +01:00
this . passedElement . triggerEvent ( EVENTS . removeItem , {
id ,
value ,
label ,
customProperties ,
groupValue : group && group . value ? group.value : null ,
} ) ;
2016-09-05 23:04:15 +02:00
}
2018-05-29 20:55:33 +02:00
_addChoice ( {
2017-08-15 13:50:37 +02:00
value ,
label = null ,
isSelected = false ,
isDisabled = false ,
groupId = - 1 ,
2019-12-23 19:22:54 +01:00
customProperties = { } ,
2017-08-15 13:50:37 +02:00
placeholder = false ,
2019-12-23 19:22:54 +01:00
keyCode = - 1 ,
} : {
value : string ;
label? : string | null ;
isSelected? : boolean ;
isDisabled? : boolean ;
groupId? : number ;
customProperties? : Record < string , any > ;
placeholder? : boolean ;
keyCode? : number ;
} ) : void {
2017-05-18 10:36:33 +02:00
if ( typeof value === 'undefined' || value === null ) {
return ;
}
2016-09-05 23:04:15 +02:00
// Generate unique id
2019-10-21 21:03:57 +02:00
const { choices } = this . _store ;
2016-09-05 23:04:15 +02:00
const choiceLabel = label || value ;
const choiceId = choices ? choices . length + 1 : 1 ;
2019-10-22 18:08:43 +02:00
const choiceElementId = ` ${ this . _baseId } - ${ this . _idNames . itemChoice } - ${ choiceId } ` ;
2016-09-05 23:04:15 +02:00
2018-05-24 10:22:07 +02:00
this . _store . dispatch (
2018-05-29 20:55:33 +02:00
addChoice ( {
id : choiceId ,
2017-07-19 21:47:42 +02:00
groupId ,
2018-05-29 20:55:33 +02:00
elementId : choiceElementId ,
2019-11-08 10:19:18 +01:00
value ,
label : choiceLabel ,
disabled : isDisabled ,
2017-07-19 21:47:42 +02:00
customProperties ,
2017-08-02 15:05:26 +02:00
placeholder ,
2017-08-15 10:29:42 +02:00
keyCode ,
2018-05-29 20:55:33 +02:00
} ) ,
2017-07-19 21:47:42 +02:00
) ;
2016-09-05 23:04:15 +02:00
2016-09-21 14:32:21 +02:00
if ( isSelected ) {
2018-05-29 20:55:33 +02:00
this . _addItem ( {
2017-06-28 11:11:02 +02:00
value ,
2018-05-29 20:55:33 +02:00
label : choiceLabel ,
2017-06-28 11:11:02 +02:00
choiceId ,
2017-07-19 19:48:46 +02:00
customProperties ,
2017-08-02 15:05:26 +02:00
placeholder ,
2017-08-15 10:29:42 +02:00
keyCode ,
2018-05-29 20:55:33 +02:00
} ) ;
2016-05-08 01:02:52 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_addGroup ( { group , id , valueKey = 'value' , labelKey = 'label' } ) : void {
const groupChoices : Choice [ ] | HTMLOptionElement [ ] = isType ( 'Object' , group )
2018-05-28 14:55:44 +02:00
? group . choices
: Array . from ( group . getElementsByTagName ( 'OPTION' ) ) ;
2017-08-15 10:29:42 +02:00
const groupId = id || Math . floor ( new Date ( ) . valueOf ( ) * Math . random ( ) ) ;
2016-09-05 23:04:15 +02:00
const isDisabled = group . disabled ? group.disabled : false ;
if ( groupChoices ) {
2019-11-03 14:18:16 +01:00
this . _store . dispatch (
addGroup ( {
value : group.label ,
id : groupId ,
active : true ,
disabled : isDisabled ,
} ) ,
) ;
2016-09-05 23:04:15 +02:00
2019-12-23 19:22:54 +01:00
const addGroupChoices = ( choice : any ) : void = > {
2018-05-28 14:55:44 +02:00
const isOptDisabled =
choice . disabled || ( choice . parentNode && choice . parentNode . disabled ) ;
2018-05-29 20:55:33 +02:00
this . _addChoice ( {
value : choice [ valueKey ] ,
label : isType ( 'Object' , choice ) ? choice [ labelKey ] : choice . innerHTML ,
isSelected : choice.selected ,
isDisabled : isOptDisabled ,
2017-06-30 15:45:47 +02:00
groupId ,
2018-05-29 20:55:33 +02:00
customProperties : choice.customProperties ,
placeholder : choice.placeholder ,
} ) ;
2017-11-11 14:40:18 +01:00
} ;
groupChoices . forEach ( addGroupChoices ) ;
2016-09-05 23:04:15 +02:00
} else {
2018-05-24 10:22:07 +02:00
this . _store . dispatch (
2019-11-03 14:18:16 +01:00
addGroup ( {
value : group.label ,
id : group.id ,
active : false ,
disabled : group.disabled ,
} ) ,
2017-07-19 21:47:42 +02:00
) ;
2016-04-16 18:06:27 +02:00
}
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_getTemplate ( template : string , . . . args : any ) : any {
2021-12-23 17:59:48 +01:00
return this . _templates [ template ] . call ( this , this . config , . . . args ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_createTemplates ( ) : void {
2018-05-25 09:53:58 +02:00
const { callbackOnCreateTemplates } = this . config ;
2016-09-30 09:40:06 +02:00
let userTemplates = { } ;
2018-05-25 09:53:58 +02:00
2018-05-28 14:55:44 +02:00
if (
callbackOnCreateTemplates &&
2019-10-29 22:19:56 +01:00
typeof callbackOnCreateTemplates === 'function'
2018-05-28 14:55:44 +02:00
) {
2018-05-25 09:53:58 +02:00
userTemplates = callbackOnCreateTemplates . call ( this , strToEl ) ;
2016-09-30 09:40:06 +02:00
}
2017-01-01 16:32:09 +01:00
2019-12-23 19:22:54 +01:00
this . _templates = merge ( templates , userTemplates ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_createElements ( ) : void {
2018-05-21 18:01:03 +02:00
this . containerOuter = new Container ( {
2018-05-29 21:08:05 +02:00
element : this._getTemplate (
'containerOuter' ,
this . _direction ,
this . _isSelectElement ,
this . _isSelectOneElement ,
this . config . searchEnabled ,
this . passedElement . element . type ,
) ,
2018-05-21 18:01:03 +02:00
classNames : this.config.classNames ,
2019-12-23 19:22:54 +01:00
type : this . passedElement . element . type as PassedElement [ 'type' ] ,
2018-05-21 18:01:03 +02:00
position : this.config.position ,
} ) ;
this . containerInner = new Container ( {
2018-05-29 21:08:05 +02:00
element : this._getTemplate ( 'containerInner' ) ,
2018-05-21 18:01:03 +02:00
classNames : this.config.classNames ,
2019-12-23 19:22:54 +01:00
type : this . passedElement . element . type as PassedElement [ 'type' ] ,
2018-05-21 18:01:03 +02:00
position : this.config.position ,
} ) ;
this . input = new Input ( {
2019-10-10 10:49:00 +02:00
element : this._getTemplate ( 'input' , this . _placeholderValue ) ,
2018-05-21 18:01:03 +02:00
classNames : this.config.classNames ,
2019-12-23 19:22:54 +01:00
type : this . passedElement . element . type as PassedElement [ 'type' ] ,
2019-10-29 18:35:20 +01:00
preventPaste : ! this . config . paste ,
2018-05-21 18:01:03 +02:00
} ) ;
this . choiceList = new List ( {
2018-05-29 21:08:05 +02:00
element : this._getTemplate ( 'choiceList' , this . _isSelectOneElement ) ,
2018-05-21 18:01:03 +02:00
} ) ;
this . itemList = new List ( {
2018-05-29 21:08:05 +02:00
element : this._getTemplate ( 'itemList' , this . _isSelectOneElement ) ,
2018-05-21 18:01:03 +02:00
} ) ;
this . dropdown = new Dropdown ( {
2018-05-29 21:08:05 +02:00
element : this._getTemplate ( 'dropdown' ) ,
2018-05-21 18:01:03 +02:00
classNames : this.config.classNames ,
2019-12-23 19:22:54 +01:00
type : this . passedElement . element . type as PassedElement [ 'type' ] ,
2018-05-21 18:01:03 +02:00
} ) ;
2018-04-24 14:57:31 +02:00
}
2016-09-05 23:04:15 +02:00
2019-12-23 19:22:54 +01:00
_createStructure ( ) : void {
2018-04-24 14:57:31 +02:00
// Hide original element
2017-10-13 14:43:58 +02:00
this . passedElement . conceal ( ) ;
2016-09-05 23:04:15 +02:00
// Wrap input in container preserving DOM ordering
2017-12-11 15:40:38 +01:00
this . containerInner . wrap ( this . passedElement . element ) ;
2016-09-05 23:04:15 +02:00
// Wrapper inner container with outer container
2017-12-11 15:40:38 +01:00
this . containerOuter . wrap ( this . containerInner . element ) ;
2016-09-05 23:04:15 +02:00
2018-05-24 10:22:07 +02:00
if ( this . _isSelectOneElement ) {
2018-05-28 14:55:44 +02:00
this . input . placeholder = this . config . searchPlaceholderValue || '' ;
2018-05-24 10:22:07 +02:00
} else if ( this . _placeholderValue ) {
this . input . placeholder = this . _placeholderValue ;
2019-11-03 18:45:16 +01:00
this . input . setWidth ( ) ;
2016-04-22 20:45:50 +02:00
}
2016-03-21 19:53:26 +01:00
2017-08-16 17:31:47 +02:00
this . containerOuter . element . appendChild ( this . containerInner . element ) ;
this . containerOuter . element . appendChild ( this . dropdown . element ) ;
2018-04-24 14:57:31 +02:00
this . containerInner . element . appendChild ( this . itemList . element ) ;
2016-08-14 23:14:37 +02:00
2018-05-24 10:22:07 +02:00
if ( ! this . _isTextElement ) {
2018-04-24 14:57:31 +02:00
this . dropdown . element . appendChild ( this . choiceList . element ) ;
2016-09-05 23:04:15 +02:00
}
2016-04-29 18:11:20 +02:00
2018-05-24 10:22:07 +02:00
if ( ! this . _isSelectOneElement ) {
2017-08-21 18:09:55 +02:00
this . containerInner . element . appendChild ( this . input . element ) ;
2018-05-23 14:09:45 +02:00
} else if ( this . config . searchEnabled ) {
2018-05-28 14:55:44 +02:00
this . dropdown . element . insertBefore (
this . input . element ,
this . dropdown . element . firstChild ,
) ;
2016-09-05 23:04:15 +02:00
}
2016-07-02 14:04:38 +02:00
2018-05-24 10:22:07 +02:00
if ( this . _isSelectElement ) {
2019-11-03 18:45:16 +01:00
this . _highlightPosition = 0 ;
this . _isSearching = false ;
2019-11-08 10:19:18 +01:00
this . _startLoading ( ) ;
2019-11-03 18:45:16 +01:00
if ( this . _presetGroups . length ) {
this . _addPredefinedGroups ( this . _presetGroups ) ;
} else {
this . _addPredefinedChoices ( this . _presetChoices ) ;
}
2019-11-08 10:19:18 +01:00
this . _stopLoading ( ) ;
2017-11-07 15:08:55 +01:00
}
2016-08-14 23:14:37 +02:00
2019-11-03 18:45:16 +01:00
if ( this . _isTextElement ) {
this . _addPredefinedItems ( this . _presetItems ) ;
}
}
2016-04-14 15:54:47 +02:00
2019-12-23 19:22:54 +01:00
_addPredefinedGroups (
groups : Group [ ] | HTMLOptGroupElement [ ] | Element [ ] ,
) : void {
2019-11-03 18:45:16 +01:00
// If we have a placeholder option
2019-12-23 19:22:54 +01:00
const placeholderChoice = ( this . passedElement as WrappedSelect )
. placeholderOption ;
2019-11-03 18:45:16 +01:00
if (
placeholderChoice &&
2019-12-23 19:22:54 +01:00
placeholderChoice . parentNode &&
( placeholderChoice . parentNode as HTMLElement ) . tagName === 'SELECT'
2019-11-03 18:45:16 +01:00
) {
this . _addChoice ( {
value : placeholderChoice.value ,
label : placeholderChoice.innerHTML ,
isSelected : placeholderChoice.selected ,
isDisabled : placeholderChoice.disabled ,
placeholder : true ,
} ) ;
}
2017-11-07 15:08:55 +01:00
2021-12-17 22:26:52 +01:00
groups . forEach ( ( group ) = >
2019-11-03 18:45:16 +01:00
this . _addGroup ( {
group ,
id : group.id || null ,
} ) ,
) ;
}
2017-10-13 10:17:20 +02:00
2019-12-23 19:22:54 +01:00
_addPredefinedChoices ( choices : Partial < Choice > [ ] ) : void {
2019-11-03 18:45:16 +01:00
// If sorting is enabled or the user is searching, filter choices
if ( this . config . shouldSort ) {
choices . sort ( this . config . sorter ) ;
}
2017-11-07 15:08:55 +01:00
2021-12-17 22:26:52 +01:00
const hasSelectedChoice = choices . some ( ( choice ) = > choice . selected ) ;
2019-11-07 09:50:19 +01:00
const firstEnabledChoiceIndex = choices . findIndex (
2021-12-17 22:26:52 +01:00
( choice ) = > choice . disabled === undefined || ! choice . disabled ,
2019-11-07 09:50:19 +01:00
) ;
2016-05-10 10:02:59 +02:00
2019-11-03 18:45:16 +01:00
choices . forEach ( ( choice , index ) = > {
2019-12-23 19:22:54 +01:00
const { value = '' , label , customProperties , placeholder } = choice ;
2016-04-29 16:18:53 +02:00
2019-11-03 18:45:16 +01:00
if ( this . _isSelectElement ) {
// If the choice is actually a group
if ( choice . choices ) {
this . _addGroup ( {
group : choice ,
id : choice.id || null ,
} ) ;
2017-11-07 15:08:55 +01:00
} else {
2019-11-07 09:50:19 +01:00
/ * *
* If there is a selected choice already or the choice is not the first in
* the array , add each choice normally .
*
* Otherwise we pre - select the first enabled choice in the array ( "select-one" only )
* /
2019-11-03 18:45:16 +01:00
const shouldPreselect =
2019-11-07 09:50:19 +01:00
this . _isSelectOneElement &&
! hasSelectedChoice &&
index === firstEnabledChoiceIndex ;
2019-11-03 18:45:16 +01:00
const isSelected = shouldPreselect ? true : choice . selected ;
2019-11-07 09:50:19 +01:00
const isDisabled = choice . disabled ;
2019-11-03 18:45:16 +01:00
2018-05-29 20:55:33 +02:00
this . _addChoice ( {
value ,
label ,
2019-12-23 19:22:54 +01:00
isSelected : ! ! isSelected ,
isDisabled : ! ! isDisabled ,
placeholder : ! ! placeholder ,
2018-05-29 20:55:33 +02:00
customProperties ,
} ) ;
2016-09-05 23:04:15 +02:00
}
2019-11-03 18:45:16 +01:00
} else {
this . _addChoice ( {
value ,
label ,
2019-12-23 19:22:54 +01:00
isSelected : ! ! choice . selected ,
isDisabled : ! ! choice . disabled ,
placeholder : ! ! choice . placeholder ,
2019-11-03 18:45:16 +01:00
customProperties ,
} ) ;
}
} ) ;
2016-09-05 23:04:15 +02:00
}
2019-12-23 19:22:54 +01:00
_addPredefinedItems ( items : Item [ ] | string [ ] ) : void {
2021-12-17 22:26:52 +01:00
items . forEach ( ( item ) = > {
2019-11-03 18:45:16 +01:00
if ( typeof item === 'object' && item . value ) {
2018-05-29 20:55:33 +02:00
this . _addItem ( {
value : item.value ,
label : item.label ,
choiceId : item.id ,
customProperties : item.customProperties ,
placeholder : item.placeholder ,
} ) ;
2019-11-03 18:45:16 +01:00
}
if ( typeof item === 'string' ) {
2018-05-29 20:55:33 +02:00
this . _addItem ( {
value : item ,
} ) ;
2017-11-07 15:08:55 +01:00
}
2019-11-03 18:45:16 +01:00
} ) ;
2017-11-07 15:08:55 +01:00
}
2019-12-23 19:22:54 +01:00
_setChoiceOrItem ( item : any ) : void {
2017-11-11 14:40:18 +01:00
const itemType = getType ( item ) . toLowerCase ( ) ;
const handleType = {
2019-12-23 19:22:54 +01:00
object : ( ) : void = > {
2017-11-11 14:40:18 +01:00
if ( ! item . value ) {
return ;
}
// If we are dealing with a select input, we need to create an option first
// that is then selected. For text inputs we can just add items normally.
2018-05-24 10:22:07 +02:00
if ( ! this . _isTextElement ) {
2018-05-29 20:55:33 +02:00
this . _addChoice ( {
value : item.value ,
label : item.label ,
isSelected : true ,
isDisabled : false ,
customProperties : item.customProperties ,
placeholder : item.placeholder ,
} ) ;
2017-11-11 14:40:18 +01:00
} else {
2018-05-29 20:55:33 +02:00
this . _addItem ( {
value : item.value ,
label : item.label ,
choiceId : item.id ,
customProperties : item.customProperties ,
placeholder : item.placeholder ,
} ) ;
2017-11-11 14:40:18 +01:00
}
} ,
2019-12-23 19:22:54 +01:00
string : ( ) : void = > {
2018-05-24 10:22:07 +02:00
if ( ! this . _isTextElement ) {
2018-05-29 20:55:33 +02:00
this . _addChoice ( {
value : item ,
label : item ,
isSelected : true ,
isDisabled : false ,
} ) ;
2017-11-11 14:40:18 +01:00
} else {
2018-05-29 20:55:33 +02:00
this . _addItem ( {
value : item ,
} ) ;
2017-11-11 14:40:18 +01:00
}
} ,
} ;
handleType [ itemType ] ( ) ;
}
2019-12-23 19:22:54 +01:00
_findAndSelectChoiceByValue ( value : string ) : void {
2019-10-21 21:03:57 +02:00
const { choices } = this . _store ;
2017-11-11 14:40:18 +01:00
// Check 'value' property exists and the choice isn't already selected
2021-12-17 22:26:52 +01:00
const foundChoice = choices . find ( ( choice ) = >
2019-12-23 19:22:54 +01:00
this . config . valueComparer ( choice . value , value ) ,
2018-05-28 14:55:44 +02:00
) ;
2017-11-11 14:40:18 +01:00
if ( foundChoice && ! foundChoice . selected ) {
2018-05-29 20:55:33 +02:00
this . _addItem ( {
value : foundChoice.value ,
label : foundChoice.label ,
2018-11-25 13:48:49 +01:00
choiceId : foundChoice.id ,
2018-05-29 20:55:33 +02:00
groupId : foundChoice.groupId ,
customProperties : foundChoice.customProperties ,
placeholder : foundChoice.placeholder ,
keyCode : foundChoice.keyCode ,
} ) ;
2017-11-11 14:40:18 +01:00
}
}
2019-12-23 19:22:54 +01:00
_generatePlaceholderValue ( ) : string | null {
if (
this . _isSelectElement &&
( this . passedElement as WrappedSelect ) . placeholderOption
) {
const { placeholderOption } = this . passedElement as WrappedSelect ;
2019-11-02 14:49:33 +01:00
2019-12-23 19:22:54 +01:00
return placeholderOption ? placeholderOption.text : null ;
2019-11-02 14:49:33 +01:00
}
const { placeholder , placeholderValue } = this . config ;
const {
element : { dataset } ,
} = this . passedElement ;
if ( placeholder ) {
if ( placeholderValue ) {
return placeholderValue ;
}
if ( dataset . placeholder ) {
return dataset . placeholder ;
}
2018-05-23 14:09:45 +02:00
}
2019-12-23 19:22:54 +01:00
return null ;
2018-05-23 14:09:45 +02:00
}
2016-08-14 23:14:37 +02:00
}
2016-03-15 23:42:10 +01:00
2019-10-22 18:08:43 +02:00
export default Choices ;