ldx-widgets
Version:
widgets
391 lines (302 loc) • 10.1 kB
text/coffeescript
React = require 'react'
createClass = require 'create-react-class'
PropTypes = require 'prop-types'
{StyleSheet, css} = require 'aphrodite/no-important'
assign = require 'lodash/assign'
dialogueMixin = require '../mixins/dialogue_mixin'
{ESCAPE, KEY_S} = require '../constants/keyboard'
ConfirmSave = React.createFactory(require('./confirm_save'))
Spinner = React.createFactory(require('./spinner'))
{div, header, span, button} = require 'react-dom-factories'
###&
@props.title - OPTIONAL - [String]
title for the modal header
@props.buttons - OPTIONAL - [Array]
Array of button objects with name, handler to be called on click, and disabled boolean, eg...
```
[
{
name: 'Save'
handler: @save
disabled: no
}
]
```
@props.children - REQUIRED - [Element]
React element (or array of elements) to inserted as the modal body
@props.styleOverride - OPTIONAL - [Object]
aphrodite style object, optionally can contain .modal, .header, .title, .actionButton class defs, eg...
```
{
modalBase: {}
modalEnter: {}
modal: {}
header: {}
title: {}
actionButton: {}
}
```
@props.close - OPTIONAL - [Function]
Function that closes the overlay, passed automatically by the overlay framework
@props.onClose - OPTIONAL - [Function]
Function that is called right before the modal closes
@props.showClose - OPTIONAL - [Boolean]
Defaults to yes, set it to no to not show the close button
@props.closeAfterSave - OPTIONAL - [Boolean]
Defaults to yes, set it to no to prevent the modal from calling @props.close after a save is complete
Note: in this case you MUST pass an onSaveComplete handler that sets the saveState to null after a save
@props.onSaveComplete - OPTIONAL - [Function]
Function that is called right after the saveState is set to complete and the success indicator finishes animating
@props.closeBtnText - OPTIONAL - [String]
Defaults to 'Cancel', text to display in the close button
@props.animateIn - OPTIONAL - [Boolean]
Defaults to yes, whether or not to bouce the modal on enter
@props.loading - OPTIONAL - [Boolean]
Defaults to no, whether or not to show the spinner instead of the children
@props.draggable - OPTIONAL - [Boolean]
Defaults to yes, whether or not the modal can be dragged from it's header
@props.stopPropagation - OPTIONAL - [Boolean]
Defaults to yes, whether or not the modal stop click from bubbling above it
@props.displayProgressBar - OPTIONAL - [Boolean]
Defaults to no, whether or not the confirm/save show the progress bar instead of the spinner
@props.uploadProgress -OPTIONAL
Progress of a file being uploaded
&###
Modal = createClass
displayName: 'Modal'
mixins: [dialogueMixin]
propTypes:
styleOverride: PropTypes.shape
modal: PropTypes.object
header: PropTypes.object
title: PropTypes.object
actionButton: PropTypes.object
title: PropTypes.string
buttons: PropTypes.array
close: PropTypes.func.isRequired
onClose: PropTypes.func
onSaveComplete: PropTypes.func
showClose: PropTypes.bool
closeAfterSave: PropTypes.bool
closeBtnText: PropTypes.string
unSavedMessage: PropTypes.string
unSavedDialogueHeight: PropTypes.number
unSavedChanges: PropTypes.bool
onSaveFail: PropTypes.func
inLineStyle: PropTypes.object
animateIn: PropTypes.bool
loading: PropTypes.bool
draggable: PropTypes.bool
stopPropagation: PropTypes.bool
spinnerProps: PropTypes.object
displayProgressBar: PropTypes.bool
uploadProgress: PropTypes.oneOfType [
PropTypes.string
PropTypes.number
]
getDefaultProps: ->
styleOverride: {}
inLineStyle: {}
buttons: []
showClose: yes
closeAfterSave: yes
unSavedDialogueHeight: 100
saveState: null
saveMessage: null
unSavedChanges: no
animateIn: yes
loading: no
draggable: yes
stopPropagation: yes
spinnerProps:
length: 7
radius: 7
lines: 12
width: 2
displayProgressBar: no
uploadProgress: ''
getInitialState: ->
dragX: 0
dragY: 0
componentWillMount: ->
# This is necessary becasue translation is not available on app load
# So this cannot live in default props
@closeBtnText = t 'Cancel'
document.addEventListener 'keydown', @handleKeyPress
componentDidMount: ->
setTimeout =>
do @calculateMaxDrags
, 0
window.addEventListener 'resize', @calculateMaxDrags
componentWillUnmount: ->
document.removeEventListener 'keydown', @handleKeyPress
window.removeEventListener 'resize', @calculateMaxDrags
render: ->
{styleOverride, title, buttons, closeBtnText, showClose, children, saveState, saveMessage,
onSaveFail, inLineStyle, animateIn, loading, spinnerProps, draggable, stopPropagation, displayProgressBar, uploadProgress} = @props
{dragX, dragY} = @state
closeBtnText = closeBtnText or @closeBtnText
assign styles, styleOverride
inLineStyle = assign {}, inLineStyle,
transform: "translate(#{dragX}px, #{dragY}px) translateZ(0px)"
WebkitTransform: "translate(#{dragX}px, #{dragY}px) translateZ(0px)"
msTransform: "translate(#{dragX}px, #{dragY}px)"
assign spinnerProps,
key: 'spinner'
headerChildren = []
# Modal Title
headerChildren.push span {
key: 'title'
className: css(styles.title)
}, title if title?
# Close Button
headerChildren.push button {
key: 'close'
className: css(styles.actionButton)
onClick: @closeWithCheck
}, closeBtnText if showClose
# Other buttons
headerChildren.push button {
key: b.name
className: css(styles.actionButton)
onClick: b.handler
disabled: b.disabled
}, b.name for b in buttons by -1
animateStyle = if animateIn then styles.modalEnter else null
div {
className: css(styles.modalBase, animateStyle, styles.modal)
style: inLineStyle
onClick: if stopPropagation then @handleClick else null
}, [
header {
key: 'header'
className: css(styles.header)
onMouseDown: if draggable then @handleMouseDown else null
ref: (@header) =>
}, headerChildren
@dialogueBox()
ConfirmSave {
key: 'confirm'
done: @saveComplete
fail: -> onSaveFail?()
saveMessage: saveMessage
saveState: saveState
displayProgressBar: displayProgressBar
uploadProgress: uploadProgress
} if saveState?
if loading then Spinner(spinnerProps) else children
]
handleClick: (e) -> do e.stopPropagation
closeWithCheck: ->
{unSavedMessage, unSavedDialogueHeight, unSavedChanges} = @props
if unSavedChanges
@showDialogue
message: unSavedMessage or t 'There are unsaved changes. How do you want to proceed?'
confirmText: t 'Discard Changes'
height: unSavedDialogueHeight
confirmCallback: @close
else
do @close
close: ->
{close, onClose} = @props
onClose?()
do close
saveComplete: ->
{close, onSaveComplete, closeAfterSave} = @props
onSaveComplete?()
do close if closeAfterSave
handleKeyPress: (e) ->
{keyCode, metaKey} = e
if keyCode is ESCAPE then do @closeWithCheck
if keyCode is KEY_S and metaKey
do e.preventDefault
{buttons} = @props
if buttons[0]?.name is t 'Save' then do buttons[0].handler
handleMouseDown: (e) ->
do e.preventDefault
{dragX, dragY} = @state
@startDragX = dragX
@startDragY = dragY
@startX = e.clientX
@startY = e.clientY
document.addEventListener 'mousemove', @handleMouseMove
document.addEventListener 'mouseup', @handleMouseUp
handleMouseMove: (e) ->
do e.preventDefault
dragX = @startDragX + (e.clientX - @startX)
dragY = @startDragY + (e.clientY - @startY)
dragY = if dragY < @minDragY then @minDragY else dragY
dragY = if dragY > @maxDragY then @maxDragY else dragY
dragX = if dragX < @minDragX then @minDragX else dragX
dragX = if dragX > @maxDragX then @maxDragX else dragX
@setState {dragX, dragY}
handleMouseUp: ->
document.removeEventListener 'mousemove', @handleMouseMove
document.removeEventListener 'mouseup', @handleMouseUp
calculateMaxDrags: ->
{offsetTop, offsetLeft} = @header.parentNode
{offsetHeight, offsetWidth} = @header
{innerHeight, innerWidth} = window
@maxDragX = innerWidth - (offsetLeft + offsetWidth)
@maxDragY = innerHeight - (offsetTop + offsetHeight)
@minDragX = 0 - offsetLeft
@minDragY = 0 - offsetTop
enterKeyFrames =
'0%':
transform: 'scale(0.9)'
opacity: '0'
'40%':
opacity: '1'
transform: 'scale(1.05)'
'100%':
transform: 'scale(1)'
styles = StyleSheet.create
modalBase:
position: 'absolute'
backgroundColor: 'white'
borderRadius: '8px'
overflow: 'hidden'
modalEnter:
animationName: [enterKeyFrames]
animationDuration: '.15s'
animationIterationCount: '1'
animationTimingFunction: 'ease-out'
modal:
top: '10%'
left: '50%'
width: '600px'
height: '500px'
marginLeft: '-300px'
header:
position: 'relative'
width: '100%'
height: '44px'
lineHeight: '44px'
backgroundColor: 'rgb(246,246,246)'
borderBottom: '1px solid rgb(190,190,190)'
borderTopLeftRadius: '8px'
borderTopRightRadius: '8px'
margin: '0px'
cursor: 'pointer'
title:
display: 'inline-block'
fontSize: '16px'
fontWeight: 'normal'
textAlign: 'left'
color: 'rgb(113,113,113)'
overflow: 'hidden'
whiteSpace: 'nowrap'
textOverflow: 'ellipsis'
marginLeft: '15px'
actionButton:
float: 'right'
color: 'rgb(0,127,255)'
height: '28px'
textAlign: 'center'
lineHeight: '26px'
marginTop: '8px'
marginRight: '15px'
fontSize: '13px'
':disabled':
color: 'rgb(204,204,204)'
module.exports = Modal