diff options
author | Grégoire Aubert <gregaubert@users.noreply.github.com> | 2017-03-22 13:40:13 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-22 13:40:13 +0100 |
commit | 685a373cc4a9028fbdd09ec2765074e8ef72e408 (patch) | |
tree | 4081f070b8e704f80626740cd1650a5eb97efea0 /server/sonar-web/src/main/js/components/common/MultiSelect.js | |
parent | 926e6e3a8a76efd342b51c511426af6e4a15b765 (diff) | |
download | sonarqube-685a373cc4a9028fbdd09ec2765074e8ef72e408.tar.gz sonarqube-685a373cc4a9028fbdd09ec2765074e8ef72e408.zip |
SONAR-8844 Add tags editor on the project homepage (#1821)
Diffstat (limited to 'server/sonar-web/src/main/js/components/common/MultiSelect.js')
-rw-r--r-- | server/sonar-web/src/main/js/components/common/MultiSelect.js | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.js b/server/sonar-web/src/main/js/components/common/MultiSelect.js new file mode 100644 index 00000000000..ec8adb3ced9 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/MultiSelect.js @@ -0,0 +1,256 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import difference from 'lodash/difference'; +import MultiSelectOption from './MultiSelectOption'; +import { translate } from '../../helpers/l10n'; + +type Props = { + selectedElements: Array<string>, + elements: Array<string>, + onSearch: (string) => void, + onSelect: (string) => void, + onUnselect: (string) => void, + validateSearchInput: (string) => string +}; + +type State = { + query: string, + selectedElements: Array<string>, + unselectedElements: Array<string>, + activeIdx: number +}; + +export default class MultiSelect extends React.PureComponent { + container: HTMLElement; + searchInput: HTMLInputElement; + props: Props; + state: State = { + query: '', + selectedElements: [], + unselectedElements: [], + activeIdx: 0 + }; + + static defaultProps = { + validateSearchInput: (value: string) => value + }; + + componentDidMount() { + this.updateSelectedElements(this.props); + this.updateUnselectedElements(this.props); + this.container.addEventListener('keydown', this.handleKeyboard, true); + } + + componentWillReceiveProps(nextProps: Props) { + if ( + this.props.elements !== nextProps.elements || + this.props.selectedElements !== nextProps.selectedElements + ) { + this.updateSelectedElements(nextProps); + this.updateUnselectedElements(nextProps); + + const totalElements = this.getAllElements(nextProps, this.state).length; + if (this.state.activeIdx >= totalElements) { + this.setState({ activeIdx: totalElements - 1 }); + } + } + } + + componentDidUpdate() { + this.searchInput && this.searchInput.focus(); + } + + componentWillUnmount() { + this.container.removeEventListener('keydown', this.handleKeyboard); + } + + handleSelectChange = (item: string, selected: boolean) => { + if (selected) { + this.onSelectItem(item); + } else { + this.onUnselectItem(item); + } + }; + + handleSearchChange = ({ target }: { target: HTMLInputElement }) => { + this.onSearchQuery(this.props.validateSearchInput(target.value)); + }; + + handleElementHover = (element: string) => { + this.setState((prevState, props) => { + return { activeIdx: this.getAllElements(props, prevState).indexOf(element) }; + }); + }; + + handleKeyboard = (evt: KeyboardEvent) => { + switch (evt.keyCode) { + case 40: // down + this.setState(this.selectNextElement); + evt.preventDefault(); + break; + case 38: // up + this.setState(this.selectPreviousElement); + evt.preventDefault(); + break; + case 13: // return + if (this.state.activeIdx >= 0) { + this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]); + } + break; + } + }; + + onSearchQuery(query: string) { + this.setState({ query, activeIdx: 0 }); + this.props.onSearch(query); + } + + onSelectItem(item: string) { + if (this.isNewElement(item, this.props)) { + this.onSearchQuery(''); + } + this.props.onSelect(item); + } + + onUnselectItem(item: string) { + this.props.onUnselect(item); + } + + isNewElement(elem: string, { selectedElements, elements }: Props) { + return elem && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1; + } + + updateSelectedElements(props: Props) { + this.setState((state: State) => { + if (state.query) { + return { + selectedElements: [...props.selectedElements.filter(elem => elem.includes(state.query))] + }; + } else { + return { selectedElements: [...props.selectedElements] }; + } + }); + } + + updateUnselectedElements(props: Props) { + this.setState({ + unselectedElements: difference(props.elements, props.selectedElements) + }); + } + + getAllElements(props: Props, state: State) { + if (this.isNewElement(state.query, props)) { + return [...state.selectedElements, ...state.unselectedElements, state.query]; + } else { + return [...state.selectedElements, ...state.unselectedElements]; + } + } + + setElementActive(idx: number) { + this.setState({ activeIdx: idx }); + } + + selectNextElement = (state: State, props: Props) => { + const { activeIdx } = state; + const allElements = this.getAllElements(props, state); + if (activeIdx < 0 || activeIdx >= allElements.length - 1) { + return { activeIdx: 0 }; + } else { + return { activeIdx: activeIdx + 1 }; + } + }; + + selectPreviousElement = (state: State, props: Props) => { + const { activeIdx } = state; + const allElements = this.getAllElements(props, state); + if (activeIdx <= 0) { + const lastIdx = allElements.length - 1; + return { activeIdx: lastIdx }; + } else { + return { activeIdx: activeIdx - 1 }; + } + }; + + toggleSelect(item: string) { + if (this.props.selectedElements.indexOf(item) === -1) { + this.onSelectItem(item); + } else { + this.onUnselectItem(item); + } + } + + render() { + const { query, activeIdx, selectedElements, unselectedElements } = this.state; + const activeElement = this.getAllElements(this.props, this.state)[activeIdx]; + + return ( + <div className="multi-select" ref={div => this.container = div}> + <div className="search-box menu-search"> + <button className="search-box-submit button-clean"> + <i className="icon-search-new" /> + </button> + <input + type="search" + value={query} + className="search-box-input" + placeholder={translate('search_verb')} + onChange={this.handleSearchChange} + autoComplete="off" + ref={input => this.searchInput = input} + /> + </div> + <ul className="menu"> + {selectedElements.length > 0 && + selectedElements.map(element => ( + <MultiSelectOption + key={element} + element={element} + selected={true} + active={activeElement === element} + onSelectChange={this.handleSelectChange} + onHover={this.handleElementHover} + /> + ))} + {unselectedElements.length > 0 && + unselectedElements.map(element => ( + <MultiSelectOption + key={element} + element={element} + active={activeElement === element} + onSelectChange={this.handleSelectChange} + onHover={this.handleElementHover} + /> + ))} + {this.isNewElement(query, this.props) && + <MultiSelectOption + key={query} + element={query} + custom={true} + active={activeElement === query} + onSelectChange={this.handleSelectChange} + onHover={this.handleElementHover} + />} + </ul> + </div> + ); + } +} |