UNPKG

goldfish-search

Version:

A SharePoint 2013 inline People Search component

333 lines (287 loc) 11 kB
//https://github.com/moroshko/react-autosuggest/blob/master/LICENSE import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { inputFocused, inputBlurred, inputChanged, updateFocusedSuggestion, revealSuggestions, closeSuggestions } from './reducerAndActions'; import Autowhatever from 'react-autowhatever'; function mapStateToProps(state) { return { isFocused: state.isFocused, isCollapsed: state.isCollapsed, focusedSectionIndex: state.focusedSectionIndex, focusedSuggestionIndex: state.focusedSuggestionIndex, valueBeforeUpDown: state.valueBeforeUpDown, lastAction: state.lastAction }; } function mapDispatchToProps(dispatch) { return { inputFocused: shouldRenderSuggestions => { dispatch(inputFocused(shouldRenderSuggestions)); }, inputBlurred: () => { dispatch(inputBlurred()); }, inputChanged: (shouldRenderSuggestions, lastAction) => { dispatch(inputChanged(shouldRenderSuggestions, lastAction)); }, updateFocusedSuggestion: (sectionIndex, suggestionIndex, value) => { dispatch(updateFocusedSuggestion(sectionIndex, suggestionIndex, value)); }, revealSuggestions: () => { dispatch(revealSuggestions()); }, closeSuggestions: lastAction => { dispatch(closeSuggestions(lastAction)); } }; } class Autosuggest extends Component { static propTypes = { suggestions: PropTypes.array.isRequired, onSuggestionsUpdateRequested: PropTypes.func.isRequired, getSuggestionValue: PropTypes.func.isRequired, renderSuggestion: PropTypes.func.isRequired, inputProps: PropTypes.object.isRequired, shouldRenderSuggestions: PropTypes.func.isRequired, onSuggestionSelected: PropTypes.func.isRequired, multiSection: PropTypes.bool.isRequired, renderSectionTitle: PropTypes.func.isRequired, getSectionSuggestions: PropTypes.func.isRequired, focusInputOnSuggestionClick: PropTypes.bool.isRequired, theme: PropTypes.object.isRequired, id: PropTypes.string.isRequired, inputRef: PropTypes.func.isRequired, isFocused: PropTypes.bool.isRequired, isCollapsed: PropTypes.bool.isRequired, focusedSectionIndex: PropTypes.number, focusedSuggestionIndex: PropTypes.number, valueBeforeUpDown: PropTypes.string, lastAction: PropTypes.string, inputFocused: PropTypes.func.isRequired, inputBlurred: PropTypes.func.isRequired, inputChanged: PropTypes.func.isRequired, updateFocusedSuggestion: PropTypes.func.isRequired, revealSuggestions: PropTypes.func.isRequired, closeSuggestions: PropTypes.func.isRequired }; constructor() { super(); this.saveInput = this.saveInput.bind(this); } componentWillReceiveProps(nextProps) { if (nextProps.suggestions !== this.props.suggestions) { const { suggestions, inputProps, shouldRenderSuggestions, isCollapsed, revealSuggestions, lastAction } = nextProps; const { value } = inputProps; if (isCollapsed && lastAction !== 'click' && lastAction !== 'enter' && suggestions.length > 0 && shouldRenderSuggestions(value)) { revealSuggestions(); } } } getSuggestion(sectionIndex, suggestionIndex) { const { suggestions, multiSection, getSectionSuggestions } = this.props; if (multiSection) { return getSectionSuggestions(suggestions[sectionIndex])[suggestionIndex]; } return suggestions[suggestionIndex]; } getFocusedSuggestion() { const { focusedSectionIndex, focusedSuggestionIndex } = this.props; if (focusedSuggestionIndex === null) { return null; } return this.getSuggestion(focusedSectionIndex, focusedSuggestionIndex); } getSuggestionValueByIndex(sectionIndex, suggestionIndex, event) { const { getSuggestionValue } = this.props; return getSuggestionValue(this.getSuggestion(sectionIndex, suggestionIndex), event); } getSuggestionIndices(suggestionElement) { const sectionIndex = suggestionElement.getAttribute('data-section-index'); const suggestionIndex = suggestionElement.getAttribute('data-suggestion-index'); return { sectionIndex: (typeof sectionIndex === 'string' ? parseInt(sectionIndex, 10) : null), suggestionIndex: parseInt(suggestionIndex, 10) }; } findSuggestionElement(startNode) { let node = startNode; do { if (node.getAttribute('data-suggestion-index') !== null) { return node; } node = node.parentNode; } while (node !== null); console.error('Clicked element:', startNode); // eslint-disable-line no-console throw new Error('Couldn\'t find suggestion element'); } maybeEmitOnChange(event, newValue, method) { const { value, onChange } = this.props.inputProps; if (newValue !== value) { onChange && onChange(event, { newValue, method }); } } willRenderSuggestions() { const { suggestions, inputProps, shouldRenderSuggestions } = this.props; const { value } = inputProps; return suggestions.length > 0 && shouldRenderSuggestions(value); } saveInput(autowhatever) { if (autowhatever !== null) { const input = autowhatever.refs.input; this.input = input; this.props.inputRef(input); } } render() { const { suggestions, onSuggestionsUpdateRequested, renderSuggestion, inputProps, shouldRenderSuggestions, onSuggestionSelected, multiSection, renderSectionTitle, id, getSectionSuggestions, focusInputOnSuggestionClick, theme, isFocused, isCollapsed, focusedSectionIndex, focusedSuggestionIndex, valueBeforeUpDown, inputFocused, inputBlurred, inputChanged, updateFocusedSuggestion, revealSuggestions, closeSuggestions } = this.props; const { value, onBlur, onFocus, onKeyDown } = inputProps; const isOpen = isFocused && !isCollapsed && this.willRenderSuggestions(); const items = (isOpen ? suggestions : []); const autowhateverInputProps = { ...inputProps, onFocus: event => { if (!this.justClickedOnSuggestion) { inputFocused(shouldRenderSuggestions(value)); onFocus && onFocus(event); } }, onBlur: event => { this.onBlurEvent = event; if (!this.justClickedOnSuggestion) { inputBlurred(); onBlur && onBlur(event); if (valueBeforeUpDown !== null && value !== valueBeforeUpDown) { onSuggestionsUpdateRequested({ value, reason: 'blur' }); } } }, onChange: event => { const { value } = event.target; const { shouldRenderSuggestions, onSuggestionsUpdateRequested } = this.props; this.maybeEmitOnChange(event, value, 'type'); inputChanged(shouldRenderSuggestions(value), 'type'); onSuggestionsUpdateRequested({ value, reason: 'type' }); }, onKeyDown: (event, data) => { switch (event.key) { case 'ArrowDown': case 'ArrowUp': if (isCollapsed) { if (this.willRenderSuggestions()) { revealSuggestions(); } } else if (suggestions.length > 0) { const { newFocusedSectionIndex, newFocusedItemIndex } = data; const newValue = newFocusedItemIndex === null ? valueBeforeUpDown : this.getSuggestionValueByIndex(newFocusedSectionIndex, newFocusedItemIndex, event); updateFocusedSuggestion(newFocusedSectionIndex, newFocusedItemIndex, value); this.maybeEmitOnChange(event, newValue, event.key === 'ArrowDown' ? 'down' : 'up'); } event.preventDefault(); break; case 'Enter': { const focusedSuggestion = this.getFocusedSuggestion(); if (focusedSuggestion !== null) { closeSuggestions('enter'); onSuggestionSelected(event, { suggestion: focusedSuggestion, suggestionValue: value, method: 'enter' }); onSuggestionsUpdateRequested({ value, reason: 'enter' }); } break; } case 'Escape': if (isOpen) { // If input.type === 'search', the browser clears the input // when Escape is pressed. We want to disable this default // behaviour so that, when suggestions are shown, we just hide // them, without clearing the input. event.preventDefault(); } if (valueBeforeUpDown === null) { // Didn't interact with Up/Down if (!isOpen) { this.maybeEmitOnChange(event, '', 'escape'); onSuggestionsUpdateRequested({ value: '', reason: 'escape' }); } } else { // Interacted with Up/Down this.maybeEmitOnChange(event, valueBeforeUpDown, 'escape'); } closeSuggestions('escape'); break; } onKeyDown && onKeyDown(event); } }; const onMouseEnter = (event, { sectionIndex, itemIndex }) => { updateFocusedSuggestion(sectionIndex, itemIndex); }; const onMouseLeave = () => { updateFocusedSuggestion(null, null); }; const onMouseDown = () => { this.justClickedOnSuggestion = true; }; const onClick = event => { const { sectionIndex, suggestionIndex } = this.getSuggestionIndices(this.findSuggestionElement(event.target)); const clickedSuggestion = this.getSuggestion(sectionIndex, suggestionIndex); const clickedSuggestionValue = this.props.getSuggestionValue(clickedSuggestion, event); this.maybeEmitOnChange(event, clickedSuggestionValue, 'click'); onSuggestionSelected(event, { suggestion: clickedSuggestion, suggestionValue: clickedSuggestionValue, method: 'click' }); closeSuggestions('click'); if (focusInputOnSuggestionClick === true) { this.input.focus(); } else { inputBlurred(); onBlur && onBlur(this.onBlurEvent); } onSuggestionsUpdateRequested({ value: clickedSuggestionValue, reason: 'click' }); this.justClickedOnSuggestion = false; }; const itemProps = ({ sectionIndex, itemIndex }) => { return { 'data-section-index': sectionIndex, 'data-suggestion-index': itemIndex, onMouseEnter, onMouseLeave, onMouseDown, onTouchStart: onMouseDown, // Because on iOS `onMouseDown` is not triggered onClick }; }; const renderItem = item => renderSuggestion(item, { value, valueBeforeUpDown }); return ( <Autowhatever multiSection={multiSection} items={items} renderItem={renderItem} renderSectionTitle={renderSectionTitle} getSectionItems={getSectionSuggestions} focusedSectionIndex={focusedSectionIndex} focusedItemIndex={focusedSuggestionIndex} inputProps={autowhateverInputProps} itemProps={itemProps} theme={theme} id={id} ref={this.saveInput} /> ); } } export default connect(mapStateToProps, mapDispatchToProps)(Autosuggest);