import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM, {findDOMNode} from 'react-dom'
import Component from '../utils/Component'
import classnames from 'classnames'
import Tool from '../utils/Tool'
import Logger from '../utils/logger'
import '../style'
import 'phoenix-styles/less/modules/popover.less'
const SHOW_CLASS = 'show'
/**
* 气泡组件<br/>
* - 通过getTarget返回当前点击元素,必需。
* - 可通过placement设置气泡的显示位置, 可选top、bottom、left、right。
* - 当设置的位置不足够放置气泡,以下顺序顺延(top->bottom->left->right, bottom->top->left->right, left->right->top->bottom, right->left->top->bottom)。
* - 可通过distance设置气泡到点击对象的位置。
* - 可通过clickCallback定义气泡显隐时额外的回调函数。
* - 可通过hideCallback手动调用隐藏popover。
*
* 示例:
* ```code
* <Button phSize='lg' ref={(button)=>{this.button = button}}>按钮</Button>
* <Popover getTarget={()=>{return this.button}} placement='top'>
* <div className='ph-popover-text'>
* 一条很长的很长的气泡提示语,为了占位存在的气泡提示语。一条很长的很长的气泡提示语,为了占位存在的气泡提示语。
* </div>
* </Popover>
* ```
* ```code
* <Button phSize='lg' ref={(button)=>{this.button1 = button}}>按钮</Button>
* <Popover getTarget={()=>{return this.button1}} placement='right'>
* <ul className='ph-popover-list'>
* <li className='ph-popover-item'>未上线单店</li>
* <li className='ph-popover-item'>未上线连锁店</li>
* </ul>
* </Popover>
* ```
*
* @class Popover
* @module 提示组件
* @extends Component
* @constructor
* @since 1.0.0
* @demo popover|popover.js {展示}
* @show true
* */
export default class Popover extends Component{
static propTypes = {
/**
* 样式前缀
* @property classPrefix
* @type String
* @default 'popover'
* */
classPrefix: PropTypes.string,
/**
* 标签tagName
* @property componentTag
* @type String
* */
componentTag: PropTypes.string,
/**
* 返回气泡的目标元素
* @method getTarget
* @type Function
* @return {object} 目标元素的ref
* */
getTarget: PropTypes.func,
/**
* 气泡的位置,默认bottom
* @property placement
* @type String
* */
placement: PropTypes.string,
/**
* 气泡距离点击物的位置,默认5
* @property distance
* @type Number
* */
distance: PropTypes.number,
/**
* 气泡显隐时可执行的额外函数,自定义
* @method clickCallback
* @param {boolean} visible 当前显隐的状态
* @type Function
* */
clickCallback: PropTypes.func,
/**
* 手动隐藏popover
* @method hideCallback
* @type Function
* */
hideCallback: PropTypes.func
};
static defaultProps = {
placement: 'bottom',
distance: 5,
classPrefix:'popover',
componentTag: 'div',
classMapping : {
'top': 'top',
'bottom': 'bottom',
'left': 'left',
'right': 'right'
}
};
constructor(props, context) {
super(props, context)
new Logger('Popover')
this.documentClickHandle = this.documentClickHandle.bind(this);
this.targetClickHandle = this.targetClickHandle.bind(this)
this.adaptePlacement = {
'top': ['top', 'bottom', 'left', 'right'],
'bottom': ['bottom', 'top', 'left', 'right'],
'left': ['left', 'right', 'top', 'bottom'],
'right': ['right', 'left', 'top', 'bottom']
}
this.placement = this.adaptePlacement[props.placement]
this.placementCount = 0
this.state = {
mounted: false
}
}
getElement() {
if (!this.el) {
this.el = document.createElement('div')
document.body.appendChild(this.el)
}
return this.el
}
componentDidMount(){
this.setState({ mounted: true }, ()=>{
// 获取点击的对象target,并绑定点击事件
let target = this.props.getTarget()
if(!target) Tool.warning('Popover 必须传递 getTarget[func]!')
this.target = findDOMNode(target)
this.target.addEventListener('click', this.targetClickHandle, false)
// 将popover动态插入body
// this.renderPortal();
// document.body.appendChild(this.el)
this.bubble = findDOMNode(this.popoverMain)
setTimeout(()=>{
document.addEventListener('click', this.documentClickHandle, false)
this.getTargetPosition()
}, 300)
})
}
renderPortal() {
this.node = document.createElement('div');
document.body.appendChild(this.node);
let popoverProps = this.otherProps
popoverProps.className = classnames(this.getProperty(true), this.props.className)
popoverProps.style = this.getStyles(this.props.style)
popoverProps.ref = (popover)=>{this.popover = popover}
let element = React.createElement('div', popoverProps , this.popoverArrow(), this.popoverMain())
ReactDOM.render(element, this.node);
}
popoverArrow(){
return <div className={Tool.setPhPrefix('popover-arrow')} ref={(popover)=>{this.popoverArrow=popover}}></div>
}
popoverMain(){
return (
<div className={Tool.setPhPrefix('popover-main')} ref={(popover)=>{this.popoverMain=popover}}>
<div className={Tool.setPhPrefix('popover-content')}>
{this.props.children}
</div>
</div>
)
}
hideCallback(){
this.removeClass(this.popover, SHOW_CLASS)
}
targetClickHandle(){
let {clickCallback} = this.props
if(this.hasClass(this.popover, SHOW_CLASS)){
this.removeClass(this.popover, SHOW_CLASS)
}else{
this.addClass(this.popover, SHOW_CLASS)
}
if(clickCallback) clickCallback(!!this.hasClass(this.popover, SHOW_CLASS))
}
documentClickHandle(event){
event.stopPropagation()
let el = event.target
if(el==this.target || Tool.contains(this.target,el) || Tool.contains(this.bubble,el)) return
this.removeClass(this.popover, SHOW_CLASS)
}
getElementLeft(element){
var actualLeft = element.offsetLeft
var current = element.offsetParent
while (current !== null){
actualLeft += current.offsetLeft
current = current.offsetParent
}
return actualLeft
}
getElementTop(element){
var actualTop = element.offsetTop
var current = element.offsetParent
while (current !== null){
actualTop += current.offsetTop
current = current.offsetParent
}
return actualTop
}
getTargetPosition(){
document.body.style.position = 'relative'
this.win = {}
this.position = {}
this.size = {}
this.bubbleSize = {}
this.win.width = parseInt(document.documentElement.clientWidth)
this.win.height = parseInt(Tool.getClientHeight())
this.position.x = parseInt(this.getElementLeft(this.target))
this.position.y = parseInt(this.getElementTop(this.target))
this.size.width = parseInt(this.target.offsetWidth)
this.size.height = parseInt(this.target.offsetHeight)
this.bubbleSize.width = parseInt(this.bubble.offsetWidth)
this.bubbleSize.height = parseInt(this.bubble.offsetHeight)
this.calcTooltipPosition(this.props.placement)
}
calcTooltipPosition(placement){
let {distance} = this.props,
topBottomLeft = this.position.x + this.size.width/2 - this.bubbleSize.width/2,
leftRightTop = this.position.y + this.size.height/2 - this.bubbleSize.height/2
this.style = {}
this.placementCount ++
let place = this.placement[this.placementCount]
switch(placement){
case 'top':
this.style.top = this.position.y - this.bubbleSize.height - distance
this.style.left = topBottomLeft
if(this.style.top<0){
this.calcTooltipPosition(place)
return
}
if(topBottomLeft + this.bubbleSize.width >= this.win.width){
this.style.left = this.win.width - this.bubbleSize.width - 2
}
if(this.style.left<0) this.style.left=0
break;
case 'bottom':
this.style.top = this.position.y + this.size.height + distance
this.style.left = topBottomLeft;
if(this.style.top>this.win.height-this.bubbleSize.height){
this.calcTooltipPosition(place)
return
}
if(topBottomLeft + this.bubbleSize.width >= this.win.width){
this.style.left = this.win.width - this.bubbleSize.width - 2
}
if(this.style.left<0) this.style.left=0
break;
case 'left':
this.style.left = this.position.x - this.bubbleSize.width - distance
this.style.top = leftRightTop
if(this.style.left<0){
this.calcTooltipPosition(place)
return
}
if(this.style.top<0) this.style.top=0
break;
case 'right':
this.style.left = this.position.x + this.size.width + distance;
this.style.top = leftRightTop;
if(this.style.left>this.win.width-this.bubbleSize.width){
this.calcTooltipPosition(place)
return
}
if(this.style.top<0) this.style.top=0
break;
default:
this.style.top = 0;
this.style.left = 0;
}
this.calcArrowPosition(placement)
}
calcArrowPosition(placement){
let {distance} = this.props,
topBottomLeft = this.position.x + this.size.width/2,
leftRightTop = this.position.y + this.size.height/2
this.arrowStyle = {};
switch(placement){
case 'top':
this.arrowStyle.top = this.position.y - distance;
this.arrowStyle.left = topBottomLeft;
break;
case 'bottom':
this.arrowStyle.top = this.position.y + this.size.height + distance;
this.arrowStyle.left = topBottomLeft;
break;
case 'left':
this.arrowStyle.left = this.position.x - distance;
this.arrowStyle.top = leftRightTop;
break;
case 'right':
this.arrowStyle.left = this.position.x + this.size.width + distance;
this.arrowStyle.top = leftRightTop;
break;
default:
this.arrowStyle.top = 0;
this.arrowStyle.left = 0;
}
this.setPopoverStyle(placement)
}
setPopoverStyle(placement){
if(this.popoverArrow){
this.popoverArrow.style.top = this.arrowStyle.top+'px'
this.popoverArrow.style.left = this.arrowStyle.left+'px'
}
if(this.popoverMain){
this.popoverMain.style.top = this.style.top+'px'
this.popoverMain.style.left = this.style.left+'px'
}
if(this.popover && this.props.placement !== placement){
this.addClass(this.popover, Tool.setPhPrefix('popover-'+placement))
this.removeClass(this.popover, Tool.setPhPrefix('popover-'+this.props.placement))
}
}
componentWillUnmount(){
this.target.removeEventListener('click', this.targetClickHandle, false)
document.removeEventListener('click', this.documentClickHandle, false)
// document.body.removeChild(this.el)
if (this.el) {
this.el.remove()
}
}
renderPopover(props) {
if (!this.state.mounted) {
return null
}
const {className, style, children} = props
return ReactDOM.createPortal(
(
<div className={classnames(this.getProperty(true), className)}
style={this.getStyles(style)}
ref={(popover)=>{this.popover = popover}}>
<div className={Tool.setPhPrefix('popover-arrow')} ref={(popover)=>{this.popoverArrow=popover}}></div>
<div className={Tool.setPhPrefix('popover-main')} ref={(popover)=>{this.popoverMain=popover}}>
<div className={Tool.setPhPrefix('popover-content')}>
{children}
</div>
</div>
</div>
),
this.getElement()
)
}
render() {
return this.renderPopover(this.props)
}
}