import classNames from "classnames" ; import * as React from "react" ; import { AbstractPureComponent2 , Classes as CoreClasses , DISPLAYNAME_PREFIX , IPopoverProps , TagInputProps , Keys , Popover , PopoverInteractionKind , Position , TagInput , TagInputAddMethod , refHandler , setRef , } from "@blueprintjs/core" ; import { Classes , IListItemsProps } from "../../common" ; import { IQueryListRendererProps , QueryList } from "../query-list/queryList" ; export type MultiSelectProps < T > = IMultiSelectProps < T > ; deprecated export interface IMultiSelectProps < T > extends IListItemsProps < T > { /**
     * Whether the component should take up the full width of its container.
     * This overrides `popoverProps.fill` and `tagInputProps.fill`.
     */ fill ? : boolean ; /**
     * Callback invoked when an item is removed from the selection by
     * removing its tag in the TagInput. This is generally more useful than
     * `tagInputProps.onRemove`  because it receives the removed value instead of
     * the value's rendered `ReactNode` tag.
     *
     * It is not recommended to supply _both_ this prop and `tagInputProps.onRemove`.
     */ onRemove ? : ( value : T , index : number ) => void ; default openOnKeyDown ? : boolean ; default placeholder ? : string ; /** Props to spread to `Popover`. Note that `content` cannot be changed. */ popoverProps ? : Partial < IPopoverProps > & object ; /** Controlled selected values. */ selectedItems ? : T [ ] ; /** Props to spread to `TagInput`. Use `query` and `onQueryChange` to control the input. */ tagInputProps ? : Partial < TagInputProps > & object ; /** Custom renderer to transform an item into tag content. */ tagRenderer : ( item : T ) => React . ReactNode ; } export interface IMultiSelectState { isOpen : boolean ; } export class MultiSelect < T > extends AbstractPureComponent2 < MultiSelectProps < T > , IMultiSelectState > { public static displayName = `${ DISPLAYNAME_PREFIX }.MultiSelect` ; public static defaultProps = { fill : false , placeholder : "Search..." , } ; public static ofType < U > ( ) { return MultiSelect as new ( props ) => MultiSelect < U > ; } public state : IMultiSelectState = { isOpen : ( this . props . popoverProps && this . props . popoverProps . isOpen ) || false , } ; private TypedQueryList = QueryList . ofType < T > ( ) ; public input : HTMLInputElement | null = null ; public queryList : QueryList < T > | null = null ; private refHandlers : { input : React . RefCallback < HTMLInputElement > ; queryList : React . RefCallback < QueryList < T > > ; } = { input : refHandler ( this , "input" , this . props . tagInputProps ?. inputRef ) , queryList : ( ref ) => ( this . queryList = ref ) , } ; public componentDidUpdate ( prevProps ) { if ( prevProps . tagInputProps ?. inputRef !== this . props . tagInputProps ?. inputRef ) { setRef ( prevProps . tagInputProps ?. inputRef , null ) ; this . refHandlers . input = refHandler ( this , "input" , this . props . tagInputProps ?. inputRef ) ; setRef ( this . props . tagInputProps ?. inputRef , this . input ) ; } } public render ( ) { const { openOnKeyDown , popoverProps , tagInputProps , ... restProps } = this . props ; return ( < this . TypedQueryList { ... restProps } onItemSelect = { this . handleItemSelect } onQueryChange = { this . handleQueryChange } ref = { this . refHandlers . queryList } renderer = { this . renderQueryList } / > ) ; } private renderQueryList = ( listProps ) => { const { fill , tagInputProps = { } , popoverProps = { } , selectedItems = [ ] , placeholder } = this . props ; const { handlePaste , handleKeyDown , handleKeyUp } = listProps ; if ( fill ) { popoverProps . fill = true ; tagInputProps . fill = true ; } const inputProps = { ... tagInputProps . inputProps , className : classNames ( tagInputProps . inputProps ?. className , Classes . MULTISELECT_TAG_INPUT_INPUT ) , } ; const handleTagInputAdd = ( values , method ) => { if ( method === "paste" ) { handlePaste ( values ) ; } } ; return ( < Popover autoFocus = { false } canEscapeKeyClose = { true } enforceFocus = { false } isOpen = { this . state . isOpen } position = { Position . BOTTOM_LEFT } { ... popoverProps } className = { classNames ( listProps . className , popoverProps . className ) } interactionKind = { PopoverInteractionKind . CLICK } onInteraction = { this . handlePopoverInteraction } popoverClassName = { classNames ( Classes . MULTISELECT_POPOVER , popoverProps . popoverClassName ) } onOpened = { this . handlePopoverOpened } > < div onKeyDown = { this . getTagInputKeyDownHandler ( handleKeyDown ) } onKeyUp = { this . getTagInputKeyUpHandler ( handleKeyUp ) } > < TagInput placeholder = { placeholder } { ... tagInputProps } className = { classNames ( Classes . MULTISELECT , tagInputProps . className ) } inputRef = { this . refHandlers . input } inputProps = { inputProps } inputValue = { listProps . query } onAdd = { handleTagInputAdd } onInputChange = { listProps . handleQueryChange } onRemove = { this . handleTagRemove } values = { selectedItems . map ( this . props . tagRenderer ) } / > < /div> < div onKeyDown = { handleKeyDown } onKeyUp = { handleKeyUp } > { listProps . itemList } < /div> { } < /Popover>); } ; private handleItemSelect = ( item , evt ? ) => { if ( this . input != null ) { this . input . focus ( ) ; } this . props . onItemSelect ?. ( item , evt ) ; } ; private handleQueryChange = ( query , evt ? ) => { this . setState ( { isOpen : query . length > 0 || ! this . props . openOnKeyDown } ) ; this . props . onQueryChange ?. ( query , evt ) ; } ; private handlePopoverInteraction = ( nextOpenState , evt ? ) => this . requestAnimationFrame ( ( ) => { const isInputFocused = this . input === document . activeElement ; if ( this . input != null && ! isInputFocused ) { this . setState ( { isOpen : false } ) ; } else if ( ! this . props . openOnKeyDown ) { this . setState ( { isOpen : true } ) ; } this . props . popoverProps ?. onInteraction ?. ( nextOpenState , evt ) ; } ) ; private handlePopoverOpened = ( node ) => { if ( this . queryList != null ) { this . queryList . scrollActiveItemIntoView ( ) ; } this . props . popoverProps ?. onOpened ?. ( node ) ; } ; private handleTagRemove = ( tag , index ) => { const { selectedItems = [ ] , onRemove , tagInputProps } = this . props ; onRemove ?. ( selectedItems [ index ] , index ) ; tagInputProps ?. onRemove ?. ( tag , index ) ; } ; private getTagInputKeyDownHandler = ( handleQueryListKeyDown ) => { return ( e ) => { const { which } = e ; if ( which === Keys . ESCAPE || which === Keys . TAB ) { if ( this . input != null ) { this . input . blur ( ) ; } this . setState ( { isOpen : false } ) ; } else if ( ! ( which === Keys . BACKSPACE || which === Keys . ARROW_LEFT || which === Keys . ARROW_RIGHT ) ) { this . setState ( { isOpen : true } ) ; } const isTargetingTagRemoveButton = ( e . target as HTMLElement ) . closest ( `.${ CoreClasses . TAG_REMOVE }` ) != null ; if ( this . state . isOpen && ! isTargetingTagRemoveButton ) { handleQueryListKeyDown ?. ( e ) ; } } ; } ; private getTagInputKeyUpHandler = ( handleQueryListKeyUp ) => { return ( e ) => { const isTargetingInput = ( e . target as HTMLElement ) . classList . contains ( Classes . MULTISELECT_TAG_INPUT_INPUT ) ; if ( this . state . isOpen && isTargetingInput ) { handleQueryListKeyUp ?. ( e ) ; } } ; } ; }