UNPKG

8.99 kBJavaScriptView Raw
1import React from 'react'
2import ReactDOM from 'react-dom'
3import PropTypes from 'prop-types'
4import classnames from 'classnames'
5import { PureComponent } from '../component'
6import { getPosition } from '../utils/dom/popover'
7import { isFunc } from '../utils/is'
8import { getParent } from '../utils/dom/element'
9import { popoverClass } from '../styles'
10import { docSize } from '../utils/dom/document'
11import isDOMElement from '../utils/dom/isDOMElement'
12import { consumer, Provider } from './context'
13
14const emptyEvent = e => e.stopPropagation()
15
16class Panel extends PureComponent {
17 constructor(props) {
18 super(props)
19
20 this.state = { show: props.defaultVisible || false }
21 this.isRendered = false
22
23 this.placeholderRef = this.placeholderRef.bind(this)
24 this.clickAway = this.clickAway.bind(this)
25 this.handleShow = this.handleShow.bind(this)
26 this.handleHide = this.handleHide.bind(this)
27 this.setShow = this.setShow.bind(this)
28 this.childStateChange = this.childStateChange.bind(this)
29
30 this.element = document.createElement('div')
31 }
32
33 componentDidMount() {
34 super.componentDidMount()
35
36 this.parentElement = this.placeholder.parentElement
37 this.bindEvents()
38 this.container = this.getContainer()
39 this.container.appendChild(this.element)
40
41 if (this.props.visible) this.forceUpdate()
42 }
43
44 componentDidUpdate(prevProps) {
45 if (this.props.trigger !== prevProps.trigger) {
46 this.bindEvents()
47 }
48 }
49
50 componentWillUnmount() {
51 super.componentWillUnmount()
52
53 this.parentElement.removeEventListener('mouseenter', this.handleShow)
54 this.parentElement.removeEventListener('mouseleave', this.handleHide)
55 this.parentElement.removeEventListener('click', this.handleShow)
56
57 document.removeEventListener('click', this.clickAway)
58 if (this.container === document.body) {
59 this.container.removeChild(this.element)
60 } else {
61 this.container.parentElement.removeChild(this.container)
62 }
63 }
64
65 setShow(show) {
66 const { onVisibleChange, mouseEnterDelay, mouseLeaveDelay, trigger, onChildStateChange } = this.props
67 const delay = show ? mouseEnterDelay : mouseLeaveDelay
68 if (onChildStateChange) onChildStateChange(show)
69 this.delayTimeout = setTimeout(
70 () => {
71 if (onVisibleChange) onVisibleChange(show)
72 this.setState({ show })
73 if (show && this.props.onOpen) this.props.onOpen()
74 if (!show && this.props.onClose) this.props.onClose()
75 },
76 trigger === 'hover' ? delay : 0
77 )
78 }
79
80 getPositionStr() {
81 let { position } = this.props
82 const { priorityDirection } = this.props
83 if (position) return position
84
85 const rect = this.parentElement.getBoundingClientRect()
86 const horizontalPoint = rect.left + rect.width / 2
87 const verticalPoint = rect.top + rect.height / 2
88 const windowHeight = docSize.height
89 const windowWidth = docSize.width
90
91 if (priorityDirection === 'horizontal') {
92 if (horizontalPoint > windowWidth / 2) position = 'left'
93 else position = 'right'
94
95 if (verticalPoint > windowHeight * 0.6) {
96 position += '-bottom'
97 } else if (verticalPoint < windowHeight * 0.4) {
98 position += '-top'
99 }
100 } else {
101 if (verticalPoint > windowHeight / 2) position = 'top'
102 else position = 'bottom'
103
104 if (horizontalPoint > windowWidth * 0.6) {
105 position += '-right'
106 } else if (horizontalPoint < windowWidth * 0.4) {
107 position += '-left'
108 }
109 }
110 // if (rect.top + rect.height / 2 > windowHeight / 2) {
111 // position = 'top'
112 // } else {
113 // position = 'bottom'
114 // }
115
116 // if (centerPoint > windowWidth * 0.6) position += '-right'
117 // else if (centerPoint < windowWidth * 0.3) position += '-left'
118
119 return position
120 }
121
122 getContainer() {
123 const { getPopupContainer } = this.props
124 let container
125 if (getPopupContainer) container = getPopupContainer()
126 if (container && isDOMElement(container)) {
127 const child = document.createElement('div')
128 child.setAttribute('style', ' position: absolute; top: 0px; left: 0px; width: 100% ')
129 return container.appendChild(child)
130 }
131 return document.body
132 }
133
134 updatePosition(position) {
135 const pos = getPosition(position, this.parentElement, this.container)
136 // eslint-disable-next-line
137 Object.keys(pos).forEach(attr => {
138 this.element.style[attr] = pos[attr]
139 })
140 }
141
142 bindEvents() {
143 const { trigger } = this.props
144 if (trigger === 'hover') {
145 this.parentElement.addEventListener('mouseenter', this.handleShow)
146 this.parentElement.addEventListener('mouseleave', this.handleHide)
147 this.element.addEventListener('mouseenter', this.handleShow)
148 this.element.addEventListener('mouseleave', this.handleHide)
149 this.parentElement.removeEventListener('click', this.handleShow)
150 } else {
151 this.parentElement.addEventListener('click', this.handleShow)
152 this.parentElement.removeEventListener('mouseenter', this.handleShow)
153 this.parentElement.removeEventListener('mouseleave', this.handleHide)
154 this.element.removeEventListener('mouseenter', this.handleShow)
155 this.element.removeEventListener('mouseleave', this.handleHide)
156 }
157 }
158
159 placeholderRef(el) {
160 this.placeholder = el
161 }
162
163 clickAway(e) {
164 if (this.parentElement.contains(e.target)) return
165 if (this.element.contains(e.target)) return
166 if (getParent(e.target, `.${popoverClass('_')}`)) return
167 this.handleHide(0)
168 }
169
170 childStateChange(state) {
171 this.childStatus = state
172 }
173
174 bindScrollDismiss(show) {
175 const { scrollDismiss } = this.props
176 if (!scrollDismiss) return
177 let target = document
178 if (typeof scrollDismiss === 'function') target = scrollDismiss()
179 const method = show ? target.addEventListener : target.removeEventListener
180 method.call(target, 'scroll', this.handleHide)
181 }
182
183 handleShow() {
184 if (this.delayTimeout) clearTimeout(this.delayTimeout)
185 if (this.state.show) return
186 this.bindScrollDismiss(true)
187 document.addEventListener('mousedown', this.clickAway)
188 this.setShow(true)
189 }
190
191 handleHide(e) {
192 const { parentClose } = this.props
193 if (this.childStatus) return
194 if (e && getParent(e.relatedTarget, `.${popoverClass('inner')}`)) return
195 if (this.delayTimeout) clearTimeout(this.delayTimeout)
196 document.removeEventListener('mousedown', this.clickAway)
197 this.bindScrollDismiss(false)
198 this.setShow(false)
199 if (parentClose) parentClose()
200 }
201
202 render() {
203 const { background, border, children, type, visible, showArrow, parentClose } = this.props
204 const show = typeof visible === 'boolean' ? visible : this.state.show
205
206 if ((!this.isRendered && !show) || !this.parentElement) {
207 return <noscript ref={this.placeholderRef} />
208 }
209
210 this.isRendered = true
211
212 const colorStyle = { background, borderColor: border }
213 const innerStyle = Object.assign({}, this.props.style, { background })
214 const position = this.getPositionStr()
215 this.element.className = classnames(popoverClass('_', position, type, parentClose && 'inner'), this.props.className)
216 // eslint-disable-next-line
217 const style = this.element.style
218 this.updatePosition(position)
219 style.display = show ? 'block' : 'none'
220 if (background) style.background = background
221 if (border) style.borderColor = border
222 let childrened = isFunc(children) ? children(this.handleHide) : children
223 if (typeof childrened === 'string') childrened = <span className={popoverClass('text')}>{childrened}</span>
224 const provider = {
225 parentClose: this.handleHide,
226 onChildStateChange: this.childStateChange,
227 }
228 return ReactDOM.createPortal(
229 [
230 showArrow && <div key="arrow" className={popoverClass('arrow')} style={colorStyle} />,
231 <div key="content" onClick={emptyEvent} className={popoverClass('content')} style={innerStyle}>
232 <Provider value={provider}>{childrened}</Provider>
233 </div>,
234 ],
235 this.element
236 )
237 }
238}
239
240Panel.propTypes = {
241 background: PropTypes.string,
242 border: PropTypes.string,
243 children: PropTypes.any,
244 onClose: PropTypes.func,
245 onOpen: PropTypes.func,
246 position: PropTypes.string,
247 style: PropTypes.object,
248 trigger: PropTypes.oneOf(['click', 'hover']),
249 type: PropTypes.string,
250 visible: PropTypes.bool,
251 onVisibleChange: PropTypes.func,
252 defaultVisible: PropTypes.bool,
253 mouseEnterDelay: PropTypes.number,
254 mouseLeaveDelay: PropTypes.number,
255 className: PropTypes.string,
256 priorityDirection: PropTypes.string,
257 getPopupContainer: PropTypes.func,
258 scrollDismiss: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
259 showArrow: PropTypes.bool,
260 parentClose: PropTypes.func,
261 onChildStateChange: PropTypes.func,
262}
263
264Panel.defaultProps = {
265 background: '',
266 trigger: 'hover',
267 mouseEnterDelay: 0,
268 mouseLeaveDelay: 500,
269 priorityDirection: 'vertical',
270 showArrow: true,
271}
272
273export default consumer(Panel)