index.jsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. // @flow
  15. import React from 'react'
  16. import styled from 'styled-components'
  17. import ReactDOM from 'react-dom'
  18. import DropdownButton from '../../atoms/DropdownButton'
  19. import Palette from '../../styleUtils/Palette'
  20. import StyleProps from '../../styleUtils/StyleProps'
  21. const Wrapper = styled.div`
  22. position: relative;
  23. `
  24. const getWidth = props => {
  25. if (props.large) {
  26. return StyleProps.inputSizes.large.width - 2
  27. }
  28. if (props.width) {
  29. return props.width - 2
  30. }
  31. return StyleProps.inputSizes.regular.width - 2
  32. }
  33. const List = styled.div`
  34. position: absolute;
  35. background: white;
  36. cursor: pointer;
  37. width: ${props => getWidth(props)}px;
  38. border: 1px solid ${Palette.grayscale[3]};
  39. border-radius: ${StyleProps.borderRadius};
  40. z-index: 1000;
  41. `
  42. const ListItems = styled.div`
  43. max-height: 400px;
  44. overflow: auto;
  45. `
  46. const Tip = styled.div`
  47. position: absolute;
  48. width: 10px;
  49. height: 10px;
  50. background: ${props => props.primary ? Palette.primary : 'white'};
  51. border-top: 1px solid ${Palette.grayscale[3]};
  52. border-left: 1px solid ${Palette.grayscale[3]};
  53. border-bottom: 1px solid ${props => props.primary ? Palette.primary : 'white'};
  54. border-right: 1px solid ${props => props.primary ? Palette.primary : 'white'};
  55. transform: rotate(45deg);
  56. right: 8px;
  57. top: -6px;
  58. z-index: 11;
  59. transition: all ${StyleProps.animations.swift};
  60. `
  61. const ListItem = styled.div`
  62. position: relative;
  63. color: ${Palette.grayscale[4]};
  64. padding: 8px 16px;
  65. transition: all ${StyleProps.animations.swift};
  66. ${props => props.selected ? `font-weight: ${StyleProps.fontWeights.medium};` : ''}
  67. &:first-child {
  68. border-top-left-radius: ${StyleProps.borderRadius};
  69. border-top-right-radius: ${StyleProps.borderRadius};
  70. }
  71. &:last-child {
  72. border-bottom-left-radius: ${StyleProps.borderRadius};
  73. border-bottom-right-radius: ${StyleProps.borderRadius};
  74. }
  75. &:hover {
  76. background: ${Palette.primary};
  77. color: white;
  78. }
  79. `
  80. type Props = {
  81. selectedItem: any,
  82. items: any[],
  83. labelField: string,
  84. className: string,
  85. onChange: (item: any) => void,
  86. noItemsMessage: string,
  87. noSelectionMessage: string,
  88. disabled: boolean,
  89. width: number,
  90. }
  91. type State = {
  92. showDropdownList: boolean,
  93. firstItemHover: boolean
  94. }
  95. class Dropdown extends React.Component<Props, State> {
  96. static defaultProps: $Shape<Props> = {
  97. noSelectionMessage: 'Select an item',
  98. }
  99. buttonRef: HTMLElement
  100. listRef: HTMLElement
  101. tipRef: HTMLElement
  102. buttonRect: ClientRect
  103. itemMouseDown: boolean
  104. constructor() {
  105. super()
  106. this.state = {
  107. showDropdownList: false,
  108. firstItemHover: false,
  109. }
  110. const self: any = this
  111. self.handlePageClick = this.handlePageClick.bind(this)
  112. }
  113. componentDidMount() {
  114. window.addEventListener('mousedown', this.handlePageClick, false)
  115. if (this.buttonRef) this.buttonRect = this.buttonRef.getBoundingClientRect()
  116. }
  117. componentWillUpdate() {
  118. if (this.buttonRef) this.buttonRect = this.buttonRef.getBoundingClientRect()
  119. }
  120. componentDidUpdate() {
  121. this.updateListPosition()
  122. }
  123. componentWillUnmount() {
  124. window.removeEventListener('mousedown', this.handlePageClick, false)
  125. }
  126. getLabel(item: any) {
  127. let labelField = this.props.labelField || 'label'
  128. if (item === null || item === undefined) {
  129. return this.props.noSelectionMessage
  130. }
  131. return (item[labelField] !== null && item[labelField] !== undefined && item[labelField].toString()) || item.toString()
  132. }
  133. handlePageClick() {
  134. if (!this.itemMouseDown) {
  135. this.setState({ showDropdownList: false })
  136. }
  137. }
  138. handleButtonClick() {
  139. if (this.props.disabled) {
  140. return
  141. }
  142. this.setState({ showDropdownList: !this.state.showDropdownList })
  143. }
  144. handleItemClick(item: any) {
  145. this.setState({ showDropdownList: false, firstItemHover: false })
  146. if (this.props.onChange) {
  147. this.props.onChange(item)
  148. }
  149. }
  150. handleItemMouseEnter(index: number) {
  151. if (index === 0) {
  152. this.setState({ firstItemHover: true })
  153. }
  154. }
  155. handleItemMouseLeave(index: number) {
  156. if (index === 0) {
  157. this.setState({ firstItemHover: false })
  158. }
  159. }
  160. updateListPosition() {
  161. if (!this.state.showDropdownList || !this.listRef || !this.buttonRef || !document.body) {
  162. return
  163. }
  164. let buttonHeight = this.buttonRef.offsetHeight
  165. let tipHeight = 8
  166. let listTop = this.buttonRect.top + buttonHeight + tipHeight
  167. let listHeight = this.listRef.offsetHeight
  168. if (listTop + listHeight > window.innerHeight) {
  169. listTop = window.innerHeight - listHeight - 10
  170. this.tipRef.style.display = 'none'
  171. } else {
  172. this.tipRef.style.display = 'block'
  173. }
  174. // If a modal is opened, body scroll is removed and body top is set to replicate scroll position
  175. let scrollOffset = 0
  176. if (parseInt(document.body.style.top, 10) < 0) {
  177. scrollOffset = -parseInt(document.body && document.body.style.top, 10)
  178. }
  179. this.listRef.style.top = `${listTop + (window.pageYOffset || scrollOffset)}px`
  180. this.listRef.style.left = `${this.buttonRect.left}px`
  181. }
  182. renderList() {
  183. if (!this.props.items || this.props.items.length === 0 || !this.state.showDropdownList) {
  184. return null
  185. }
  186. const body: any = document.body
  187. let selectedLabel = this.getLabel(this.props.selectedItem)
  188. let list = ReactDOM.createPortal((
  189. <List {...this.props} innerRef={ref => { this.listRef = ref }}>
  190. <Tip innerRef={ref => { this.tipRef = ref }} primary={this.state.firstItemHover} />
  191. <ListItems>
  192. {this.props.items.map((item, i) => {
  193. let label = this.getLabel(item)
  194. let listItem = (
  195. <ListItem
  196. key={label}
  197. onMouseDown={() => { this.itemMouseDown = true }}
  198. onMouseUp={() => { this.itemMouseDown = false }}
  199. onMouseEnter={() => { this.handleItemMouseEnter(i) }}
  200. onMouseLeave={() => { this.handleItemMouseLeave(i) }}
  201. onClick={() => { this.handleItemClick(item) }}
  202. selected={label === selectedLabel}
  203. >{label}
  204. </ListItem>
  205. )
  206. return listItem
  207. })}
  208. </ListItems>
  209. </List>
  210. ), body)
  211. return list
  212. }
  213. render() {
  214. let buttonValue = () => {
  215. if (this.props.items && this.props.items.length) {
  216. return this.getLabel(this.props.selectedItem)
  217. }
  218. return this.props.noItemsMessage || ''
  219. }
  220. return (
  221. <Wrapper
  222. className={this.props.className}
  223. onMouseDown={() => { this.itemMouseDown = true }}
  224. onMouseUp={() => { this.itemMouseDown = false }}
  225. >
  226. <DropdownButton
  227. {...this.props}
  228. innerRef={ref => { this.buttonRef = ref }}
  229. onMouseDown={() => { this.itemMouseDown = true }}
  230. onMouseUp={() => { this.itemMouseDown = false }}
  231. value={buttonValue()}
  232. onClick={() => this.handleButtonClick()}
  233. />
  234. {this.renderList()}
  235. </Wrapper>
  236. )
  237. }
  238. }
  239. export default Dropdown