import * as React from "react" ; import * as ReactDOM from "react-dom" ; import { KeyCodes } from "./KeyCodes" ; import * as DOMUtils from "../Utils/DOMUtils" ; const MenuCssClass = "dropdown-menu" ; const MenuItemCssClass = "menu-item" ; const HighlightCssClass = "highlight" ; export class DropdownMenu { private el : HTMLElement ; private ops : DropdownMenuOptions ; param anchor param items param options param callback constructor ( anchor , items , options , private callback ) { this . ops = { ... DefaultDropdownMenuOptions , ... options } ; this . render ( anchor , items ) ; this . attachEventHandlers ( ) ; this . el . focus ( ) ; } private render ( anchor , items ) { this . el = document . createElement ( "div" ) ; this . el . classList . add ( MenuCssClass ) ; this . el . setAttribute ( "tabindex" , "0" ) ; this . ops . parent . appendChild ( this . el ) ; const anchorOffset = DOMUtils . getOffset ( anchor ) ; const menuTop = anchorOffset . top + anchor . offsetHeight + this . ops . verticalOffset ; this . el . style . left = anchorOffset . left + 'px' ; this . el . style . top = menuTop + 'px' ; if ( items && items . length ) { const labels = items . map ( value => value . toString ( ) ) ; const element = React . createElement ( DropdownMenuPanel , { items : labels } ) ; ReactDOM . render ( element , this . el ) ; } } private attachEventHandlers ( ) { this . el . addEventListener ( 'blur' , ( ) => this . close ( ) ) ; this . el . addEventListener ( 'keydown' , ev => this . onKeyDown ( ev ) ) ; this . el . addEventListener ( 'mouseover' , ev => this . onMouseOverItem ( ev ) ) ; this . el . addEventListener ( 'mouseout' , ( ) => this . onMouseOut ( ) ) ; this . el . addEventListener ( 'mousedown' , ( ) => this . onMouseDown ( ) ) ; } private removeHighlight ( ) { const current = this . el . querySelector ( '.' + HighlightCssClass ) ; if ( current ) { current . classList . remove ( HighlightCssClass ) ; } } private onMouseOverItem ( ev ) { const target = ev . target as HTMLElement ; if ( target . classList . contains ( MenuItemCssClass ) ) { this . removeHighlight ( ) ; target . classList . add ( HighlightCssClass ) ; } } private onMouseOut ( ) { this . removeHighlight ( ) ; } private onMouseDown ( ) { const current = this . el . querySelector ( '.' + HighlightCssClass ) ; this . onItemSelected ( current ) ; } private onKeyDown ( ev ) { switch ( ev . keyCode ) { case KeyCodes . Escape : this . close ( ) ; break ; case KeyCodes . Enter : this . onItemSelected ( this . el . querySelector ( '.' + HighlightCssClass ) ) ; break ; case KeyCodes . UpArrow : case KeyCodes . DownArrow : const allItems = Array . from ( this . el . querySelectorAll ( '.' + MenuItemCssClass ) ) ; const current = this . el . querySelector ( '.' + HighlightCssClass ) ; const index = allItems . indexOf ( current ) ; let newIndex ; if ( ev . keyCode === KeyCodes . UpArrow ) newIndex = ( index === - 1 || index === 0 ) ? allItems . length - 1 : index - 1 ; else newIndex = ( index === - 1 || index === allItems . length - 1 ) ? 0 : index + 1 ; if ( current ) current . classList . remove ( HighlightCssClass ) ; allItems [ newIndex ] . classList . add ( HighlightCssClass ) ; break ; } ev . preventDefault ( ) ; ev . stopPropagation ( ) ; } private onItemSelected ( item ) { if ( item ) { const allItems = Array . from ( this . el . querySelectorAll ( '.' + MenuItemCssClass ) ) ; const index = allItems . indexOf ( item ) ; if ( index !== - 1 ) { this . close ( ) ; this . callback ( index ) ; } } } private close ( ) { const el = this . el ; this . el = null ; if ( el ) { el . remove ( ) ; } } } export interface DropdownMenuOptions { /** How much the menu must be offset relative to the bottom of the anchor element. */ verticalOffset ? : number ; /** Parent element for the menu; if not specified menu will be added to body. */ parent ? : HTMLElement ; } export const DefaultDropdownMenuOptions = { verticalOffset : 0 , parent : document . body } ; interface DropdownMenuPanelProps { items : string [ ] ; } function DropdownMenuPanel ( props ) { const items = props . items . map ( ( item , i ) => < div key = { i } className = "menu-item" > { item } < /div>); return ( < div > { items } < /div>); }