import * as React from "react" ; import { AbstractComponent2 , DISPLAYNAME_PREFIX , Props , Keys , Menu , Utils } from "@blueprintjs/core" ; import { executeItemsEqual , getActiveItem , getCreateNewItem , ICreateNewItem , IItemListRendererProps , IItemModifiers , IListItemsProps , isCreateNewItem , renderFilteredItems , } from "../../common" ; export type QueryListProps < T > = IQueryListProps < T > ; deprecated export interface IQueryListProps < T > extends IListItemsProps < T > { /**
     * Initial active item, useful if the parent component is controlling its selectedItem but
     * not activeItem.
     */ initialActiveItem ? : T ; /**
     * Callback invoked when user presses a key, after processing `QueryList`'s own key events
     * (up/down to navigate active item). This callback is passed to `renderer` and (along with
     * `onKeyUp`) can be attached to arbitrary content elements to support keyboard selection.
     */ onKeyDown ? : React . KeyboardEventHandler < HTMLElement > ; /**
     * Callback invoked when user releases a key, after processing `QueryList`'s own key events
     * (enter to select active item). This callback is passed to `renderer` and (along with
     * `onKeyDown`) can be attached to arbitrary content elements to support keyboard selection.
     */ onKeyUp ? : React . KeyboardEventHandler < HTMLElement > ; /**
     * Customize rendering of the component.
     * Receives an object with props that should be applied to elements as necessary.
     */ renderer : ( listProps : IQueryListRendererProps < T > ) => JSX . Element ; default disabled ? : boolean ; } /**
 * An object describing how to render a `QueryList`.
 * A `QueryList` `renderer` receives this object as its sole argument.
 */ export interface IQueryListRendererProps < T > extends Pick < IQueryListState < T > , "activeItem" | "filteredItems" | "query" > , Props { /**
     * Selection handler that should be invoked when a new item has been chosen,
     * perhaps because the user clicked it.
     */ handleItemSelect : ( item : T , event ? : React . SyntheticEvent < HTMLElement > ) => void ; /**
     * Handler that should be invoked when the user pastes one or more values.
     *
     * This callback will use `itemPredicate` with `exactMatch=true` to find a
     * subset of `items` exactly matching the pasted `values` provided, then it
     * will invoke `onItemsPaste` with those found items. Each pasted value that
     * does not exactly match an item will be ignored.
     *
     * If creating items is enabled (by providing both `createNewItemFromQuery`
     * and `createNewItemRenderer`), then pasted values that do not exactly
     * match an existing item will emit a new item as created via
     * `createNewItemFromQuery`.
     *
     * If `itemPredicate` returns multiple matching items for a particular query
     * in `queries`, then only the first matching item will be emitted.
     */ handlePaste : ( queries : string [ ] ) => void ; /**
     * Keyboard handler for up/down arrow keys to shift the active item.
     * Attach this handler to any element that should support this interaction.
     */ handleKeyDown : React . KeyboardEventHandler < HTMLElement > ; /**
     * Keyboard handler for enter key to select the active item.
     * Attach this handler to any element that should support this interaction.
     */ handleKeyUp : React . KeyboardEventHandler < HTMLElement > ; /**
     * Change handler for query string. Attach this to an input element to allow
     * `QueryList` to control the query.
     */ handleQueryChange : React . ChangeEventHandler < HTMLInputElement > ; /** Rendered elements returned from `itemListRenderer` prop. */ itemList : React . ReactNode ; } export interface IQueryListState < T > { /** The currently focused item (for keyboard interactions). */ activeItem : T | ICreateNewItem | null ; /**
     * The item returned from `createNewItemFromQuery(this.state.query)`, cached
     * to avoid continuous reinstantions within `isCreateItemRendered`, where
     * this element will be used to hide the "Create Item" option if its value
     * matches the current `query`.
     */ createNewItem : T | undefined ; /** The original `items` array filtered by `itemListPredicate` or `itemPredicate`. */ filteredItems : T [ ] ; /** The current query string. */ query : string ; } export class QueryList < T > extends AbstractComponent2 < QueryListProps < T > , IQueryListState < T > > { public static displayName = `${ DISPLAYNAME_PREFIX }.QueryList` ; public static defaultProps = { disabled : false , resetOnQuery : true , } ; public static ofType < U > ( ) { return QueryList as new ( props ) => QueryList < U > ; } private itemsParentRef ? : HTMLElement | null ; private refHandlers = { itemsParent : ( ref ) => ( this . itemsParentRef = ref ) , } ; /**
     * Flag indicating that we should check whether selected item is in viewport
     * after rendering, typically because of keyboard change. Set to `true` when
     * manipulating state in a way that may cause active item to scroll away.
     */ private shouldCheckActiveItemInViewport = false ; /**
     * The item that we expect to be the next selected active item (based on click
     * or key interactions). When scrollToActiveItem = false, used to detect if
     * an unexpected external change to the active item has been made.
     */ private expectedNextActiveItem : T | ICreateNewItem | null = null ; /**
     * Flag which is set to true while in between an ENTER "keydown" event and its
     * corresponding "keyup" event.
     *
     * When entering text via an IME (https://en.wikipedia.org/wiki/Input_method),
     * the ENTER key is pressed to confirm the character(s) to be input from a list
     * of options. The operating system intercepts the ENTER "keydown" event and
     * prevents it from propagating to the application, but "keyup" is still
     * fired, triggering a spurious event which this component does not expect.
     *
     * To work around this quirk, we keep track of "real" key presses by setting
     * this flag in handleKeyDown.
     */ private isEnterKeyPressed = false ; public constructor ( props , context ? ) { super ( props , context ) ; const { query = "" } = props ; const createNewItem = props . createNewItemFromQuery ?. ( query ) ; const filteredItems = getFilteredItems ( query , props ) ; this . state = { activeItem : props . activeItem !== undefined ? props . activeItem : props . initialActiveItem ?? getFirstEnabledItem ( filteredItems , props . itemDisabled ) , createNewItem , filteredItems , query , } ; } public render ( ) { const { className , items , renderer , itemListRenderer = this . renderItemList } = this . props ; const { createNewItem , ... spreadableState } = this . state ; return renderer ( { ... spreadableState , className , handleItemSelect : this . handleItemSelect , handleKeyDown : this . handleKeyDown , handleKeyUp : this . handleKeyUp , handlePaste : this . handlePaste , handleQueryChange : this . handleInputQueryChange , itemList : itemListRenderer ( { ... spreadableState , items , itemsParentRef : this . refHandlers . itemsParent , renderCreateItem : this . renderCreateItemMenuItem , renderItem : this . renderItem , } ) , } ) ; } public componentDidUpdate ( prevProps ) { if ( this . props . activeItem !== undefined && this . props . activeItem !== this . state . activeItem ) { this . shouldCheckActiveItemInViewport = true ; this . setState ( { activeItem : this . props . activeItem } ) ; } if ( this . props . query != null && this . props . query !== prevProps . query ) { this . setQuery ( this . props . query , this . props . resetOnQuery , this . props ) ; } else if ( ! Utils . shallowCompareKeys ( this . props , prevProps , { include : [ "items" , "itemListPredicate" , "itemPredicate" ] , } ) ) { this . setQuery ( this . state . query ) ; } if ( this . shouldCheckActiveItemInViewport ) { this . requestAnimationFrame ( ( ) => this . scrollActiveItemIntoView ( ) ) ; this . shouldCheckActiveItemInViewport = false ; } } public scrollActiveItemIntoView ( ) { const scrollToActiveItem = this . props . scrollToActiveItem !== false ; const externalChangeToActiveItem = ! executeItemsEqual ( this . props . itemsEqual , getActiveItem ( this . expectedNextActiveItem ) , getActiveItem ( this . props . activeItem ) ) ; this . expectedNextActiveItem = null ; if ( ! scrollToActiveItem && externalChangeToActiveItem ) { return ; } const activeElement = this . getActiveElement ( ) ; if ( this . itemsParentRef != null && activeElement != null ) { const { offsetTop : activeTop , offsetHeight : activeHeight } = activeElement ; const { offsetTop : parentOffsetTop , scrollTop : parentScrollTop , clientHeight : parentHeight , } = this . itemsParentRef ; const { paddingTop , paddingBottom } = this . getItemsParentPadding ( ) ; const activeBottomEdge = activeTop + activeHeight + paddingBottom - parentOffsetTop ; const activeTopEdge = activeTop - paddingTop - parentOffsetTop ; if ( activeBottomEdge >= parentScrollTop + parentHeight ) { this . itemsParentRef . scrollTop = activeBottomEdge + activeHeight - parentHeight ; } else if ( activeTopEdge <= parentScrollTop ) { this . itemsParentRef . scrollTop = activeTopEdge - activeHeight ; } } } public setQuery ( query , resetActiveItem = this . props . resetOnQuery , props = this . props ) { const { createNewItemFromQuery } = props ; this . shouldCheckActiveItemInViewport = true ; const hasQueryChanged = query !== this . state . query ; if ( hasQueryChanged ) { props . onQueryChange ?. ( query ) ; } const trimmedQuery = query . trim ( ) ; const filteredItems = getFilteredItems ( trimmedQuery , props ) ; const createNewItem = createNewItemFromQuery != null && trimmedQuery !== "" ? createNewItemFromQuery ( trimmedQuery ) : undefined ; this . setState ( { createNewItem , filteredItems , query } ) ; const activeIndex = this . getActiveIndex ( filteredItems ) ; const shouldUpdateActiveItem = resetActiveItem || activeIndex < 0 || isItemDisabled ( getActiveItem ( this . state . activeItem ) , activeIndex , props . itemDisabled ) ; if ( shouldUpdateActiveItem ) { if ( this . isCreateItemRendered ( ) && this . isCreateItemFirst ( ) ) { this . setActiveItem ( getCreateNewItem ( ) ) ; } else { this . setActiveItem ( getFirstEnabledItem ( filteredItems , props . itemDisabled ) ) ; } } } public setActiveItem ( activeItem ) { this . expectedNextActiveItem = activeItem ; if ( this . props . activeItem === undefined ) { this . shouldCheckActiveItemInViewport = true ; this . setState ( { activeItem } ) ; } if ( isCreateNewItem ( activeItem ) ) { this . props . onActiveItemChange ?. ( null , true ) ; } else { this . props . onActiveItemChange ?. ( activeItem , false ) ; } } /** default `itemListRenderer` implementation */ private renderItemList = ( listProps ) => { const { initialContent , noResults } = this . props ; const createItemView = listProps . renderCreateItem ( ) ; const maybeNoResults = createItemView != null ? null : noResults ; const menuContent = renderFilteredItems ( listProps , maybeNoResults , initialContent ) ; if ( menuContent == null && createItemView == null ) { return null ; } const createFirst = this . isCreateItemFirst ( ) ; return ( < Menu ulRef = { listProps . itemsParentRef } > { createFirst && createItemView } { menuContent } { ! createFirst && createItemView } < /Menu>); } ; private renderItem = ( item , index ) => { if ( this . props . disabled !== true ) { const { activeItem , query } = this . state ; const matchesPredicate = this . state . filteredItems . indexOf ( item ) >= 0 ; const modifiers = { active : executeItemsEqual ( this . props . itemsEqual , getActiveItem ( activeItem ) , item ) , disabled : isItemDisabled ( item , index , this . props . itemDisabled ) , matchesPredicate , } ; return this . props . itemRenderer ( item , { handleClick : e => this . handleItemSelect ( item , e ) , index , modifiers , query , } ) ; } return null ; } ; private renderCreateItemMenuItem = ( ) => { if ( this . isCreateItemRendered ( ) ) { const { activeItem , query } = this . state ; const trimmedQuery = query . trim ( ) ; const handleClick = evt => { this . handleItemCreate ( trimmedQuery , evt ) ; } ; const isActive = isCreateNewItem ( activeItem ) ; return this . props . createNewItemRenderer ! ( trimmedQuery , isActive , handleClick ) ; } return null ; } ; private getActiveElement ( ) { const { activeItem } = this . state ; if ( this . itemsParentRef != null ) { if ( isCreateNewItem ( activeItem ) ) { const index = this . isCreateItemFirst ( ) ? 0 : this . state . filteredItems . length ; return this . itemsParentRef . children . item ( index ) as HTMLElement ; } else { const activeIndex = this . getActiveIndex ( ) ; return this . itemsParentRef . children . item ( activeIndex ) as HTMLElement ; } } return undefined ; } private getActiveIndex ( items = this . state . filteredItems ) { const { activeItem } = this . state ; if ( activeItem == null || isCreateNewItem ( activeItem ) ) { return - 1 ; } for ( let i = 0 ; i < items . length ; ++ i ) { if ( executeItemsEqual ( this . props . itemsEqual , items [ i ] , activeItem ) ) { return i ; } } return - 1 ; } private getItemsParentPadding ( ) { const { paddingTop , paddingBottom } = getComputedStyle ( this . itemsParentRef ! ) ; return { paddingBottom : pxToNumber ( paddingBottom ) , paddingTop : pxToNumber ( paddingTop ) , } ; } private handleItemCreate = ( query , evt ? ) => { const item = this . props . createNewItemFromQuery ?. ( query ) ; if ( item != null ) { this . props . onItemSelect ?. ( item , evt ) ; this . maybeResetQuery ( ) ; } } ; private handleItemSelect = ( item , event ? ) => { this . setActiveItem ( item ) ; this . props . onItemSelect ?. ( item , event ) ; this . maybeResetQuery ( ) ; } ; private handlePaste = ( queries ) => { const { createNewItemFromQuery , onItemsPaste } = this . props ; let nextActiveItem ; const nextQueries = [ ] ; const pastedItemsToEmit = [ ] ; for ( const query of queries ) { const equalItem = getMatchingItem ( query , this . props ) ; if ( equalItem !== undefined ) { nextActiveItem = equalItem ; pastedItemsToEmit . push ( equalItem ) ; } else if ( this . canCreateItems ( ) ) { const newItem = createNewItemFromQuery ?. ( query ) ; if ( newItem !== undefined ) { pastedItemsToEmit . push ( newItem ) ; } } else { nextQueries . push ( query ) ; } } this . setQuery ( nextQueries . join ( ", " ) , false ) ; if ( nextActiveItem !== undefined ) { this . setActiveItem ( nextActiveItem ) ; } onItemsPaste ?. ( pastedItemsToEmit ) ; } ; private handleKeyDown = ( event ) => { const { keyCode } = event ; if ( keyCode === Keys . ARROW_UP || keyCode === Keys . ARROW_DOWN ) { event . preventDefault ( ) ; const nextActiveItem = this . getNextActiveItem ( keyCode === Keys . ARROW_UP ? - 1 : 1 ) ; if ( nextActiveItem != null ) { this . setActiveItem ( nextActiveItem ) ; } } else if ( keyCode === Keys . ENTER ) { this . isEnterKeyPressed = true ; } this . props . onKeyDown ?. ( event ) ; } ; private handleKeyUp = ( event ) => { const { onKeyUp } = this . props ; const { activeItem } = this . state ; if ( event . keyCode === Keys . ENTER && this . isEnterKeyPressed ) { event . preventDefault ( ) ; if ( activeItem == null || isCreateNewItem ( activeItem ) ) { this . handleItemCreate ( this . state . query , event ) ; } else { this . handleItemSelect ( activeItem , event ) ; } this . isEnterKeyPressed = false ; } onKeyUp ?. ( event ) ; } ; private handleInputQueryChange = ( event ? ) => { const query = event == null ? "" : event . target . value ; this . setQuery ( query ) ; this . props . onQueryChange ?. ( query , event ) ; } ; private getNextActiveItem ( direction , startIndex = this . getActiveIndex ( ) ) { if ( this . isCreateItemRendered ( ) ) { const reachedCreate = ( startIndex === 0 && direction === - 1 ) || ( startIndex === this . state . filteredItems . length - 1 && direction === 1 ) ; if ( reachedCreate ) { return getCreateNewItem ( ) ; } } return getFirstEnabledItem ( this . state . filteredItems , this . props . itemDisabled , direction , startIndex ) ; } private isCreateItemRendered ( ) { return ( this . canCreateItems ( ) && this . state . query !== "" && ! this . wouldCreatedItemMatchSomeExistingItem ( ) ) ; } private isCreateItemFirst ( ) { return this . props . createNewItemPosition === "first" ; } private canCreateItems ( ) { return this . props . createNewItemFromQuery != null && this . props . createNewItemRenderer != null ; } private wouldCreatedItemMatchSomeExistingItem ( ) { return this . state . filteredItems . some ( item => executeItemsEqual ( this . props . itemsEqual , item , this . state . createNewItem ) ) ; } private maybeResetQuery ( ) { if ( this . props . resetOnSelect ) { this . setQuery ( "" , true ) ; } } } function pxToNumber ( value ) { return value == null ? 0 : parseInt ( value . slice ( 0 , - 2 ) , 10 ) ; } function getMatchingItem < T > ( query , { items , itemPredicate } ) { if ( Utils . isFunction ( itemPredicate ) ) { for ( let i = 0 ; i < items . length ; i ++ ) { const item = items [ i ] ; if ( itemPredicate ( query , item , i , true ) ) { return item ; } } } return undefined ; } function getFilteredItems < T > ( query , { items , itemPredicate , itemListPredicate } ) { if ( Utils . isFunction ( itemListPredicate ) ) { return itemListPredicate ( query , items ) ; } else if ( Utils . isFunction ( itemPredicate ) ) { return items . filter ( ( item , index ) => itemPredicate ( query , item , index ) ) ; } return items ; } /** Wrap number around min/max values: if it exceeds one bound, return the other. */ function wrapNumber ( value , min , max ) { if ( value < min ) { return max ; } else if ( value > max ) { return min ; } return value ; } function isItemDisabled < T > ( item , index , itemDisabled ? ) { if ( itemDisabled == null || item == null ) { return false ; } else if ( Utils . isFunction ( itemDisabled ) ) { return itemDisabled ( item , index ) ; } return ! ! item [ itemDisabled ] ; } param items param itemDisabled param direction param startIndex export function getFirstEnabledItem < T > ( items , itemDisabled ? , direction = 1 , startIndex = items . length - 1 ) { if ( items . length === 0 ) { return null ; } let index = startIndex ; const maxIndex = items . length - 1 ; do { index = wrapNumber ( index + direction , 0 , maxIndex ) ; if ( ! isItemDisabled ( items [ index ] , index , itemDisabled ) ) { return items [ index ] ; } } while ( index !== startIndex && startIndex !== - 1 ) ; return null ; }