ldx-widgets
Version:
widgets
365 lines (282 loc) • 11.8 kB
text/coffeescript
React = require 'react'
_ = require 'lodash'
animationMixin = require '../mixins/animation_mixin'
{div, p, button} = React.DOM
NIB_OFFSET = 12
EDGE_BUFFER = 4
###
This class wraps any passed react component in a PVR
It is intend to simplify creating any PVr within app, by handling the positioning and direction of the PVR
Popover Props
@props.element - a react element (not factory) to render inside the pvr
@props.direction - string - default 'auto'
below, above, right, left
detirmines on which side of the anchor element the pvr will appear
- in 'auto' mode below, above, right, left is the order they are checked
- should the pvr not fit in the direction provided, the oposite sirection will be used
- should the pvr not fit in the opposite direction, the other 2 will be checked
- should the pvr not fit in any direction, the direction with the most space will be used
@props.height - default 100
height of the pvr
@props.width default 200
width of the pvr
@props.styleMixin
object containing any style properties to mixin with and/or overrride the defaults
note that width height are passed separately so they can have defaults and auto settings
passing width/height in this object could cause issues
@props.anchor - DOM element
pass then e.currentTarget or e.target element through to enable auto-positioning
if this isn't passed, you'll have to manually configure positioning with styleMixin or CSS
@props.vAdjust - default 0
for above, below positioning
negative value moves the pvr closer to the anchor element, positive value moves it further away
for left, right
negative value moves pvr higher on the screen, positive value moves it lower
@props.vSlide - default 0
leave the nib in the same place, but offsets the pvr body by this number of pixels
@props.hAdjust - default 0
for above, below positioning
negative value moves the pvr further left, positive value moves it further right
for left, right
negative value moves the pvr closer to the anchor element, positive value moves it further away
@props.hSlide - default 0
leave the nib in the same place, but offsets the pvr body by this number of pixels
@props.nibColor - default white
the color of the nib
@props.showNib - default yes
whether the nib should show
@props.borderColor - default rgb(235,235,235)
the color of pvr border
@props.updateOnKeys - default none
any keystrokes that should cause a rerender of the pvr
@props.closeOnAnchorLeave - default yes
whether or not the pvr should close if the anchor element moves offscreen during a resize or key stroke (of a key in updateOnKeys)
###
Pvr = React.createClass
displayName: 'Pvr'
mixins: [animationMixin]
enterStateStart:
scale: .9
enterStateEnd:
scale: 1
enterEasing: 'easeOutElastic'
enterDuration: 600
propTypes:
close: React.PropTypes.func.isRequired
getDefaultProps: ->
{
height: 100
width: 200
styleMixin: {}
Adjust: 0
vAdjust: 0
vSlide: 0
hAdjust: 0
hSlide: 0
anchor: {
getBoundingClientRect: ->
top: 0
right: 0
bottom: 0
left: 0
}
element: null
direction: 'auto'
nibColor: 'white'
showNib: yes
borderColor: 'rgb(235,235,235)'
updateOnKeys: []
closeOnAnchorLeave: yes
}
componentWillMount: ->
{direction} = @props
@directionCheck = switch direction
when 'left' then ['left', 'right', 'below', 'above']
when 'right' then ['right', 'left', 'below', 'above']
when 'below' then ['below', 'above', 'right', 'left']
when 'above' then ['above', 'below', 'right', 'left']
else ['below', 'above', 'right', 'left']
window.addEventListener("resize", @handleResize)
window.addEventListener("keydown", @handleKeyStroke)
componentWillUnmount: ->
window.removeEventListener("resize", @handleResize)
window.removeEventListener("keydown", @handleKeyStroke)
handleKeyStroke: (e) ->
{keyCode} = e
{updateOnKeys, closeOnAnchorLeave, close} = @props
return unless keyCode in updateOnKeys
if @anchorIsOffscreen() and closeOnAnchorLeave then do close
else @forceUpdate()
handleResize: ->
{closeOnAnchorLeave} = @props
if @anchorIsOffscreen() and closeOnAnchorLeave then do close
else @forceUpdate()
anchorIsOffscreen: ->
{top, left, right, bottom} = @props.anchor.getBoundingClientRect()
if top > window.innerHeight or left > window.innerWidth or bottom < 0 or right < 0 then yes
else no
handleClick: (e) ->
e.stopPropagation()
calculateOptimalDirection: (elPos) ->
{innerHeight, innerWidth} = window
{preferredDirection, width, height} = @props
{right, left, top, bottom} = elPos
# left and top are already the space to the left and the top of the anchor
# transform right and bottom to the space to the right and bottom of the anchor
right = innerWidth - right
bottom = innerHeight - bottom
# Check if the popover fits in the preferred direction
widthNeeded = width + NIB_OFFSET + EDGE_BUFFER
heightNeeded = height + NIB_OFFSET + EDGE_BUFFER
for direction in @directionCheck
switch direction
when 'left'
if widthNeeded < left then return 'left'
when 'right'
if widthNeeded < right then return 'right'
when 'below'
if heightNeeded < bottom then return 'below'
when 'above'
if heightNeeded < top then return 'above'
# If the popover will not fit in any of the the preferred directions
# return the direction with the most space
max = _.max [left, top, bottom, right]
switch max
when left then 'left'
when right then 'right'
when top then 'above'
when bottom then 'below'
render: ->
{width, height, styleMixin, position, element, vAdjust, hAdjust, anchor, direction, nibColor, showNib, borderColor, vSlide, hSlide} = @props
scale = @props.scale or @state.scale
elPos = anchor.getBoundingClientRect()
anchorTop = elPos.top
anchorbottom = elPos.bottom
anchorLeft = elPos.left
anchorRight = elPos.right
anchorWidth = elPos.width
anchorHeight = elPos.height
direction = @calculateOptimalDirection elPos
className = "pvr #{direction}"
nibClass = "nib #{direction}"
nibBorderClass = "nib border #{direction}"
nibPosition = NIB_OFFSET/2
# When adjusting the nib center point up/down, don't exceed half the height minus border radius
maxNibVerticalAdjust = height/2 - NIB_OFFSET/2 - 5
# When adjusting the nib center point left/right, don't exceed half the width minus border radius
maxNibHorizontalAdjust = width/2 - NIB_OFFSET/2 - 5
switch direction
when 'left'
top = anchorTop + anchorHeight/2 - height/2 + vAdjust - vSlide
left = anchorLeft - NIB_OFFSET - width - hAdjust
# Make sure the pvr is not off the screen to the bottom
topAdjust = top + height + EDGE_BUFFER - window.innerHeight
if topAdjust > 0
top = top - topAdjust - vSlide
nibPosition = NIB_OFFSET/2 - Math.min(topAdjust, maxNibVerticalAdjust)
# Make sure the pvr is not off the screen to the top
topAdjust = top - EDGE_BUFFER
if topAdjust < 0
top = top - topAdjust - vSlide
nibPosition = NIB_OFFSET/2 - Math.min(topAdjust, maxNibVerticalAdjust)
nibPosition -= vSlide
nibStyle =
borderColor: "transparent transparent transparent #{nibColor}"
top: "calc(50% - #{nibPosition}px)"
nibBorderStyle =
borderColor: "transparent transparent transparent #{borderColor}"
top: "calc(50% - #{nibPosition}px)"
right: 0 - NIB_OFFSET - 1
when 'right'
top = anchorTop + anchorHeight/2 - height/2 + vAdjust - vSlide
left = anchorRight + NIB_OFFSET + hAdjust
# Make sure the pvr is not off the screen to the bottom
topAdjust = top + height + EDGE_BUFFER - window.innerHeight
if topAdjust > 0
top = top - topAdjust - vSlide
nibPosition = NIB_OFFSET/2 - Math.min(topAdjust, maxNibVerticalAdjust)
# Make sure the pvr is not off the screen to the top
topAdjust = top - EDGE_BUFFER
if topAdjust < 0
top = top - topAdjust - vSlide
nibPosition = NIB_OFFSET/2 - Math.min(topAdjust, maxNibVerticalAdjust)
nibPosition -= vSlide
nibStyle =
borderColor: "transparent #{nibColor} transparent transparent"
top: "calc(50% - #{nibPosition}px)"
nibBorderStyle =
borderColor: "transparent #{borderColor} transparent transparent"
top: "calc(50% - #{nibPosition}px)"
left: 0 - NIB_OFFSET - 1
when 'below'
top = anchorTop + anchorHeight + NIB_OFFSET + vAdjust
left = anchorLeft + anchorWidth/2 - width/2 + hAdjust - hSlide
# Make sure the pvr is not off the screen to the right
leftAdjust = left + width + EDGE_BUFFER - window.innerWidth
if leftAdjust > 0
left = left - leftAdjust - hSlide
nibPosition = NIB_OFFSET/2 - Math.min(leftAdjust, maxNibHorizontalAdjust)
# Make sure the pvr is not off the screen to the left
leftAdjust = left - EDGE_BUFFER
if leftAdjust < 0
left = left - leftAdjust - hSlide
nibPosition = NIB_OFFSET/2 - Math.min(leftAdjust, maxNibHorizontalAdjust)
nibPosition -= hSlide
nibStyle =
borderColor: "transparent transparent #{nibColor} transparent"
left: "calc(50% - #{nibPosition}px)"
nibBorderStyle =
borderColor: "transparent transparent #{borderColor} transparent"
left: "calc(50% - #{nibPosition}px)"
top: 0 - NIB_OFFSET - 1
when 'above'
top = anchorTop - NIB_OFFSET - height - vAdjust
left = anchorLeft + anchorWidth/2 - width/2 + hAdjust - hSlide
# Make sure the pvr is not off the screen to the right
leftAdjust = left + width + EDGE_BUFFER - window.innerWidth
if leftAdjust > 0
left = left - leftAdjust - hSlide
nibPosition = NIB_OFFSET/2 - Math.min(leftAdjust, maxNibHorizontalAdjust)
# Make sure the pvr is not off the screen to the left
leftAdjust = left - EDGE_BUFFER
if leftAdjust < 0
left = left - leftAdjust - hSlide
nibPosition = NIB_OFFSET/2 - Math.min(leftAdjust, maxNibHorizontalAdjust)
nibPosition -= hSlide
nibStyle =
borderColor: "#{nibColor} transparent transparent transparent"
left: "calc(50% - #{nibPosition}px)"
nibBorderStyle =
borderColor: " #{borderColor} transparent transparent transparent"
left: "calc(50% - #{nibPosition}px)"
bottom: 0 - NIB_OFFSET - 1
element = element or div {key: 'inner', className: 'inner'}
# Set the styles
style =
height: height
width: width
top: top
left: left
borderColor: borderColor
transform: "scale(#{scale})"
WebkitTransform: "scale(#{scale})"
msTransform: "scale(#{scale})"
_.assign style, styleMixin
div {
className: className
style: style
onClick: @handleClick
}, [
div {
key: 'nibBorder'
className: nibBorderClass
style: nibBorderStyle
} if showNib
div {
key:'nibInner'
className: nibClass
style: nibStyle
} if showNib
element
]
module.exports = Pvr