import * as React from 'react'; import { MmuiTooltipComponent } from '../MmuiTooltipComponent'; import { parseChoice } from './utils'; export interface Props { id?: any; title?: string; summaryRenderMode?: string; summaryTextDisplaySep?: string; hasFilterInput?: boolean; //whether there is a filter input field to visually filter option choices hasSelectAll?: boolean; /// whether there is a select all checkbox to toggle between selecting or un-selecting all choices. filterPlaceholder?: string; data?: any; //data used to construct the list of choices } export interface State { id: string; // unique id for the Component title: string; //string that is displayed if there are no option choices selected. summaryRenderMode: string; //"text" or "token", determines if the summary is displayed as text or as tokens summaryTextDisplaySep: string; //if summary render mode is "text", filterPlaceholder: string; //placeholder text for the filter input box filterValue: string; //default value for the filter input field choices: any[]; //the options in the multi-select, each can have a children list selectedChoiceCount: number; // number of parents that have been selected isOpen: boolean; //tracks whether the Component is in the expanded state or not. tooltipX: number; tooltipY: number; tooltipBody: string; hasTooltip: boolean; } export abstract class MmuiDropChoiceComponent< P extends Props > extends React.Component { protected id; protected static id_counter = 0; protected readonly expandedCharacterCount = 32; protected readonly closedCharacterCount = 13; /** * Generate a unique id for this component instance */ protected getId(): number { if (this.id === undefined) { this.id = MmuiDropChoiceComponent.id_counter; MmuiDropChoiceComponent.id_counter++; } return this.id; } /** * Constructor * @param props * props.summaryRenderMode: Options "text" or "token". * Specifying "text" the summary of selected choices is display as text. * Specifying "token" the summary of selected choices is display as tokens. */ constructor(props: P) { super(props); const state = { id: 'mmui_drop_choice_component_' + this.getId(), title: 'Select an option', summaryRenderMode: 'text', summaryTextDisplaySep: ', ', filterPlaceholder: 'Filter options', filterValue: '', choices: [], selectedChoiceCount: 0, isOpen: false, tooltipX: 0, tooltipY: 0, tooltipBody: '', hasTooltip: false, }; if (props.id) { state.id = props.id; } if (props.title) { state.title = props.title; } if (props.summaryRenderMode) { state.summaryRenderMode = props.summaryRenderMode; } if (props.summaryTextDisplaySep) { state.summaryTextDisplaySep = props.summaryTextDisplaySep; } if (props.filterPlaceholder) { state.filterPlaceholder = props.filterPlaceholder; } if (props.data) { state.choices = props.data.choices; for (const choice of state.choices) { if (choice.isChecked) { state.selectedChoiceCount = state.selectedChoiceCount + 1; } } } this.state = state; } /** * Toggles the isOpen state * @param e */ headerClick(e, config?: any) { e.preventDefault(); document.addEventListener('click', this.onBlur); const setStateCallback = config && config.setStateCallback ? config.setStateCallback : undefined; this.setState({ isOpen: !this.state.isOpen }, setStateCallback); } /** * Listens for clicks on the Drop Choice header. * @param e */ onHeaderClick = (e: React.SyntheticEvent) => { this.headerClick(e); }; /** * Detects user clicks outside of the visual area of the Component. * @param e */ onBlur = (e) => { this.blur(e); }; /** * Sets state to collapsed (isOpen == false) and cleans up the event listeners. * @param e */ blur(e, config?: any) { if (this.state.isOpen) { let elmt = event.target as HTMLElement, isFieldClick = false; while (elmt) { isFieldClick = isFieldClick || elmt.id === this.state.id; if (isFieldClick) { break; } elmt = elmt.parentElement; } if (!isFieldClick) { const setStateCallback = config && config.setStateCallback ? config.setStateCallback : undefined; this.setState({ isOpen: false }, setStateCallback); document.removeEventListener('click', this.onBlur); } } } onTokenMouseOut = () => { this.setState({ tooltipBody: '', tooltipX: 0, tooltipY: 0, hasTooltip: false, }); }; onTokenMouseOver = (e: React.SyntheticEvent) => { const tokenElmt = e.target as HTMLElement; if (tokenElmt.classList.contains('mmui-token-remove')) { return; } const componentElmt = document.getElementById(this.state.id); const tokenRect = tokenElmt.getBoundingClientRect(); const componentRect = componentElmt.getBoundingClientRect(); const x = tokenRect.x - componentRect.x; // also, subtract half the height of the tooltip + arrow const y = tokenRect.y - componentRect.y - 70; const tooltipBody = tokenElmt.dataset.tokenDisplay; if(tooltipBody){ this.setState({ tooltipBody: tooltipBody, tooltipX: x, tooltipY: y, hasTooltip: true, }); } }; abstract removeToken(choiceId); /** * Remove the target token from state * @param e: onClick event */ onTokenRemove = (e: React.SyntheticEvent) => { e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); const choiceTokenElmt = e.target as HTMLElement, choiceId = choiceTokenElmt.dataset.tokenId; this.removeToken(choiceId); }; abstract updateChoice(newChoice); /** * Listens for choice state changes * @param e */ onChoiceChange = (e: React.SyntheticEvent) => { const newChoice = parseChoice(e.target as HTMLInputElement); this.updateChoice(newChoice); }; abstract applyFilter(filterValue: string); /** * Listens for filter value changes and hides any choices with a display that doesn't contain the filter value. * @param e */ onFilterChange = (e: React.SyntheticEvent) => { const filterValue = (e.target as HTMLInputElement).value; this.applyFilter(filterValue); this.setState({ filterValue: filterValue }); }; getSelectAllCheckRender() { return; } abstract getTextSummaryRender(); abstract getTokenSummaryRender(); abstract renderChoice(choice); /** * Render the Component */ render() { const choiceInputElmts = this.state.choices.map((choice) => { return this.renderChoice(choice); } ); let summaryDisplayRender; if (this.state.summaryRenderMode === 'token') { summaryDisplayRender = this.getTokenSummaryRender(); } else { summaryDisplayRender = this.getTextSummaryRender(); } let filterInput; if (this.props.hasFilterInput) { filterInput = ( ); } const selectAllCheck = this.getSelectAllCheckRender(); let tooltip; const isOpenTooltipCheck = this.state.isOpen && this.state.tooltipBody.length > this.expandedCharacterCount; const isClosedTooltipCheck = !this.state.isOpen && this.state.tooltipBody.length > this.closedCharacterCount; if ( this.state.hasTooltip && (isOpenTooltipCheck || isClosedTooltipCheck) ) { tooltip = ( ); } return this.renderDropdown(summaryDisplayRender, filterInput, selectAllCheck, choiceInputElmts, tooltip); } renderDropdown(summaryDisplayRender, filterInput, selectAllCheck, choiceInputElmts, tooltip){ const classNameArray = ['mmui-drop-choice']; if (this.state.isOpen) { classNameArray.push('mmui-drop-choice-expanded'); } else { classNameArray.push('mmui-drop-choice-collapsed'); } const dropdownButtonClasses = ['btn', 'mmui-btn-dropdown']; const empty = this.state.choices.length < 1; return ( <> {empty ? (
) : (
{summaryDisplayRender}
{filterInput}
{selectAllCheck} {choiceInputElmts}
{tooltip}
)} ); } }