parent
4c6221929f
commit
14028655df
@ -0,0 +1,219 @@ |
|||||||
|
// Package imports.
|
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import React from 'react'; |
||||||
|
import spring from 'react-motion/lib/spring'; |
||||||
|
import Toggle from 'react-toggle'; |
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component'; |
||||||
|
import classNames from 'classnames'; |
||||||
|
|
||||||
|
// Components.
|
||||||
|
import Icon from 'flavours/glitch/components/icon'; |
||||||
|
|
||||||
|
// Utils.
|
||||||
|
import { withPassive } from 'flavours/glitch/util/dom_helpers'; |
||||||
|
import Motion from 'flavours/glitch/util/optional_motion'; |
||||||
|
import { assignHandlers } from 'flavours/glitch/util/react_helpers'; |
||||||
|
|
||||||
|
class ComposerOptionsDropdownContentItem extends ImmutablePureComponent { |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
active: PropTypes.bool, |
||||||
|
name: PropTypes.string, |
||||||
|
onChange: PropTypes.func, |
||||||
|
onClose: PropTypes.func, |
||||||
|
options: PropTypes.shape({ |
||||||
|
icon: PropTypes.string, |
||||||
|
meta: PropTypes.node, |
||||||
|
on: PropTypes.bool, |
||||||
|
text: PropTypes.node, |
||||||
|
}), |
||||||
|
}; |
||||||
|
|
||||||
|
handleActivate = (e) => { |
||||||
|
const { |
||||||
|
name, |
||||||
|
onChange, |
||||||
|
onClose, |
||||||
|
options: { on }, |
||||||
|
} = this.props; |
||||||
|
|
||||||
|
// If the escape key was pressed, we close the dropdown.
|
||||||
|
if (e.key === 'Escape' && onClose) { |
||||||
|
onClose(); |
||||||
|
|
||||||
|
// Otherwise, we both close the dropdown and change the value.
|
||||||
|
} else if (onChange && (!e.key || e.key === 'Enter')) { |
||||||
|
e.preventDefault(); // Prevents change in focus on click
|
||||||
|
if ((on === null || typeof on === 'undefined') && onClose) { |
||||||
|
onClose(); |
||||||
|
} |
||||||
|
onChange(name); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Rendering.
|
||||||
|
render () { |
||||||
|
const { |
||||||
|
active, |
||||||
|
options: { |
||||||
|
icon, |
||||||
|
meta, |
||||||
|
on, |
||||||
|
text, |
||||||
|
}, |
||||||
|
} = this.props; |
||||||
|
const computedClass = classNames('composer--options--dropdown--content--item', { |
||||||
|
active, |
||||||
|
lengthy: meta, |
||||||
|
'toggled-off': !on && on !== null && typeof on !== 'undefined', |
||||||
|
'toggled-on': on, |
||||||
|
'with-icon': icon, |
||||||
|
}); |
||||||
|
|
||||||
|
let prefix = null; |
||||||
|
|
||||||
|
if (on !== null && typeof on !== 'undefined') { |
||||||
|
prefix = <Toggle checked={on} onChange={this.handleActivate} />; |
||||||
|
} else if (icon) { |
||||||
|
prefix = <Icon className='icon' fullwidth icon={icon} /> |
||||||
|
} |
||||||
|
|
||||||
|
// The result.
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={computedClass} |
||||||
|
onClick={this.handleActivate} |
||||||
|
onKeyDown={this.handleActivate} |
||||||
|
role='button' |
||||||
|
tabIndex='0' |
||||||
|
> |
||||||
|
{prefix} |
||||||
|
|
||||||
|
<div className='content'> |
||||||
|
<strong>{text}</strong> |
||||||
|
{meta ? meta : nil} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
// The spring to use with our motion.
|
||||||
|
const springMotion = spring(1, { |
||||||
|
damping: 35, |
||||||
|
stiffness: 400, |
||||||
|
}); |
||||||
|
|
||||||
|
// The component.
|
||||||
|
export default class ComposerOptionsDropdownContent extends React.PureComponent { |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({ |
||||||
|
icon: PropTypes.string, |
||||||
|
meta: PropTypes.node, |
||||||
|
name: PropTypes.string.isRequired, |
||||||
|
on: PropTypes.bool, |
||||||
|
text: PropTypes.node, |
||||||
|
})), |
||||||
|
onChange: PropTypes.func, |
||||||
|
onClose: PropTypes.func, |
||||||
|
style: PropTypes.object, |
||||||
|
value: PropTypes.string, |
||||||
|
}; |
||||||
|
|
||||||
|
static defaultProps = { |
||||||
|
style: {}, |
||||||
|
}; |
||||||
|
|
||||||
|
state = { |
||||||
|
mounted: false, |
||||||
|
}; |
||||||
|
|
||||||
|
// When the document is clicked elsewhere, we close the dropdown.
|
||||||
|
handleDocumentClick = ({ target }) => { |
||||||
|
const { node } = this; |
||||||
|
const { onClose } = this.props; |
||||||
|
if (onClose && node && !node.contains(target)) { |
||||||
|
onClose(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Stores our node in `this.node`.
|
||||||
|
handleRef = (node) => { |
||||||
|
this.node = node; |
||||||
|
} |
||||||
|
|
||||||
|
// On mounting, we add our listeners.
|
||||||
|
componentDidMount () { |
||||||
|
document.addEventListener('click', this.handleDocumentClick, false); |
||||||
|
document.addEventListener('touchend', this.handleDocumentClick, withPassive); |
||||||
|
this.setState({ mounted: true }); |
||||||
|
} |
||||||
|
|
||||||
|
// On unmounting, we remove our listeners.
|
||||||
|
componentWillUnmount () { |
||||||
|
document.removeEventListener('click', this.handleDocumentClick, false); |
||||||
|
document.removeEventListener('touchend', this.handleDocumentClick, withPassive); |
||||||
|
} |
||||||
|
|
||||||
|
// Rendering.
|
||||||
|
render () { |
||||||
|
const { mounted } = this.state; |
||||||
|
const { |
||||||
|
items, |
||||||
|
onChange, |
||||||
|
onClose, |
||||||
|
style, |
||||||
|
value, |
||||||
|
} = this.props; |
||||||
|
|
||||||
|
// The result.
|
||||||
|
return ( |
||||||
|
<Motion |
||||||
|
defaultStyle={{ |
||||||
|
opacity: 0, |
||||||
|
scaleX: 0.85, |
||||||
|
scaleY: 0.75, |
||||||
|
}} |
||||||
|
style={{ |
||||||
|
opacity: springMotion, |
||||||
|
scaleX: springMotion, |
||||||
|
scaleY: springMotion, |
||||||
|
}} |
||||||
|
> |
||||||
|
{({ opacity, scaleX, scaleY }) => ( |
||||||
|
// It should not be transformed when mounting because the resulting
|
||||||
|
// size will be used to determine the coordinate of the menu by
|
||||||
|
// react-overlays
|
||||||
|
<div |
||||||
|
className='composer--options--dropdown--content' |
||||||
|
ref={this.handleRef} |
||||||
|
style={{ |
||||||
|
...style, |
||||||
|
opacity: opacity, |
||||||
|
transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, |
||||||
|
}} |
||||||
|
> |
||||||
|
{items ? items.map( |
||||||
|
({ |
||||||
|
name, |
||||||
|
...rest |
||||||
|
}) => ( |
||||||
|
<ComposerOptionsDropdownContentItem |
||||||
|
active={name === value} |
||||||
|
key={name} |
||||||
|
name={name} |
||||||
|
onChange={onChange} |
||||||
|
onClose={onClose} |
||||||
|
options={rest} |
||||||
|
/> |
||||||
|
) |
||||||
|
) : null} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</Motion> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,146 +0,0 @@ |
|||||||
// Package imports.
|
|
||||||
import PropTypes from 'prop-types'; |
|
||||||
import React from 'react'; |
|
||||||
import spring from 'react-motion/lib/spring'; |
|
||||||
|
|
||||||
// Components.
|
|
||||||
import ComposerOptionsDropdownContentItem from './item'; |
|
||||||
|
|
||||||
// Utils.
|
|
||||||
import { withPassive } from 'flavours/glitch/util/dom_helpers'; |
|
||||||
import Motion from 'flavours/glitch/util/optional_motion'; |
|
||||||
import { assignHandlers } from 'flavours/glitch/util/react_helpers'; |
|
||||||
|
|
||||||
// Handlers.
|
|
||||||
const handlers = { |
|
||||||
// When the document is clicked elsewhere, we close the dropdown.
|
|
||||||
handleDocumentClick ({ target }) { |
|
||||||
const { node } = this; |
|
||||||
const { onClose } = this.props; |
|
||||||
if (onClose && node && !node.contains(target)) { |
|
||||||
onClose(); |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
// Stores our node in `this.node`.
|
|
||||||
handleRef (node) { |
|
||||||
this.node = node; |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
// The spring to use with our motion.
|
|
||||||
const springMotion = spring(1, { |
|
||||||
damping: 35, |
|
||||||
stiffness: 400, |
|
||||||
}); |
|
||||||
|
|
||||||
// The component.
|
|
||||||
export default class ComposerOptionsDropdownContent extends React.PureComponent { |
|
||||||
|
|
||||||
// Constructor.
|
|
||||||
constructor (props) { |
|
||||||
super(props); |
|
||||||
assignHandlers(this, handlers); |
|
||||||
|
|
||||||
// Instance variables.
|
|
||||||
this.node = null; |
|
||||||
|
|
||||||
this.state = { |
|
||||||
mounted: false, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
// On mounting, we add our listeners.
|
|
||||||
componentDidMount () { |
|
||||||
const { handleDocumentClick } = this.handlers; |
|
||||||
document.addEventListener('click', handleDocumentClick, false); |
|
||||||
document.addEventListener('touchend', handleDocumentClick, withPassive); |
|
||||||
this.setState({ mounted: true }); |
|
||||||
} |
|
||||||
|
|
||||||
// On unmounting, we remove our listeners.
|
|
||||||
componentWillUnmount () { |
|
||||||
const { handleDocumentClick } = this.handlers; |
|
||||||
document.removeEventListener('click', handleDocumentClick, false); |
|
||||||
document.removeEventListener('touchend', handleDocumentClick, withPassive); |
|
||||||
} |
|
||||||
|
|
||||||
// Rendering.
|
|
||||||
render () { |
|
||||||
const { mounted } = this.state; |
|
||||||
const { handleRef } = this.handlers; |
|
||||||
const { |
|
||||||
items, |
|
||||||
onChange, |
|
||||||
onClose, |
|
||||||
style, |
|
||||||
value, |
|
||||||
} = this.props; |
|
||||||
|
|
||||||
// The result.
|
|
||||||
return ( |
|
||||||
<Motion |
|
||||||
defaultStyle={{ |
|
||||||
opacity: 0, |
|
||||||
scaleX: 0.85, |
|
||||||
scaleY: 0.75, |
|
||||||
}} |
|
||||||
style={{ |
|
||||||
opacity: springMotion, |
|
||||||
scaleX: springMotion, |
|
||||||
scaleY: springMotion, |
|
||||||
}} |
|
||||||
> |
|
||||||
{({ opacity, scaleX, scaleY }) => ( |
|
||||||
// It should not be transformed when mounting because the resulting
|
|
||||||
// size will be used to determine the coordinate of the menu by
|
|
||||||
// react-overlays
|
|
||||||
<div |
|
||||||
className='composer--options--dropdown--content' |
|
||||||
ref={handleRef} |
|
||||||
style={{ |
|
||||||
...style, |
|
||||||
opacity: opacity, |
|
||||||
transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, |
|
||||||
}} |
|
||||||
> |
|
||||||
{items ? items.map( |
|
||||||
({ |
|
||||||
name, |
|
||||||
...rest |
|
||||||
}) => ( |
|
||||||
<ComposerOptionsDropdownContentItem |
|
||||||
active={name === value} |
|
||||||
key={name} |
|
||||||
name={name} |
|
||||||
onChange={onChange} |
|
||||||
onClose={onClose} |
|
||||||
options={rest} |
|
||||||
/> |
|
||||||
) |
|
||||||
) : null} |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</Motion> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
// Props.
|
|
||||||
ComposerOptionsDropdownContent.propTypes = { |
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({ |
|
||||||
icon: PropTypes.string, |
|
||||||
meta: PropTypes.node, |
|
||||||
name: PropTypes.string.isRequired, |
|
||||||
on: PropTypes.bool, |
|
||||||
text: PropTypes.node, |
|
||||||
})), |
|
||||||
onChange: PropTypes.func, |
|
||||||
onClose: PropTypes.func, |
|
||||||
style: PropTypes.object, |
|
||||||
value: PropTypes.string, |
|
||||||
}; |
|
||||||
|
|
||||||
// Default props.
|
|
||||||
ComposerOptionsDropdownContent.defaultProps = { style: {} }; |
|
@ -1,129 +0,0 @@ |
|||||||
// Package imports.
|
|
||||||
import classNames from 'classnames'; |
|
||||||
import PropTypes from 'prop-types'; |
|
||||||
import React from 'react'; |
|
||||||
import Toggle from 'react-toggle'; |
|
||||||
|
|
||||||
// Components.
|
|
||||||
import Icon from 'flavours/glitch/components/icon'; |
|
||||||
|
|
||||||
// Utils.
|
|
||||||
import { assignHandlers } from 'flavours/glitch/util/react_helpers'; |
|
||||||
|
|
||||||
// Handlers.
|
|
||||||
const handlers = { |
|
||||||
|
|
||||||
// This function activates the dropdown item.
|
|
||||||
handleActivate (e) { |
|
||||||
const { |
|
||||||
name, |
|
||||||
onChange, |
|
||||||
onClose, |
|
||||||
options: { on }, |
|
||||||
} = this.props; |
|
||||||
|
|
||||||
// If the escape key was pressed, we close the dropdown.
|
|
||||||
if (e.key === 'Escape' && onClose) { |
|
||||||
onClose(); |
|
||||||
|
|
||||||
// Otherwise, we both close the dropdown and change the value.
|
|
||||||
} else if (onChange && (!e.key || e.key === 'Enter')) { |
|
||||||
e.preventDefault(); // Prevents change in focus on click
|
|
||||||
if ((on === null || typeof on === 'undefined') && onClose) { |
|
||||||
onClose(); |
|
||||||
} |
|
||||||
onChange(name); |
|
||||||
} |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
// The component.
|
|
||||||
export default class ComposerOptionsDropdownContentItem extends React.PureComponent { |
|
||||||
|
|
||||||
// Constructor.
|
|
||||||
constructor (props) { |
|
||||||
super(props); |
|
||||||
assignHandlers(this, handlers); |
|
||||||
} |
|
||||||
|
|
||||||
// Rendering.
|
|
||||||
render () { |
|
||||||
const { handleActivate } = this.handlers; |
|
||||||
const { |
|
||||||
active, |
|
||||||
options: { |
|
||||||
icon, |
|
||||||
meta, |
|
||||||
on, |
|
||||||
text, |
|
||||||
}, |
|
||||||
} = this.props; |
|
||||||
const computedClass = classNames('composer--options--dropdown--content--item', { |
|
||||||
active, |
|
||||||
lengthy: meta, |
|
||||||
'toggled-off': !on && on !== null && typeof on !== 'undefined', |
|
||||||
'toggled-on': on, |
|
||||||
'with-icon': icon, |
|
||||||
}); |
|
||||||
|
|
||||||
// The result.
|
|
||||||
return ( |
|
||||||
<div |
|
||||||
className={computedClass} |
|
||||||
onClick={handleActivate} |
|
||||||
onKeyDown={handleActivate} |
|
||||||
role='button' |
|
||||||
tabIndex='0' |
|
||||||
> |
|
||||||
{function () { |
|
||||||
|
|
||||||
// We render a `<Toggle>` if we were provided an `on`
|
|
||||||
// property, and otherwise show an `<Icon>` if available.
|
|
||||||
switch (true) { |
|
||||||
case on !== null && typeof on !== 'undefined': |
|
||||||
return ( |
|
||||||
<Toggle |
|
||||||
checked={on} |
|
||||||
onChange={handleActivate} |
|
||||||
/> |
|
||||||
); |
|
||||||
case !!icon: |
|
||||||
return ( |
|
||||||
<Icon |
|
||||||
className='icon' |
|
||||||
fullwidth |
|
||||||
icon={icon} |
|
||||||
/> |
|
||||||
); |
|
||||||
default: |
|
||||||
return null; |
|
||||||
} |
|
||||||
}()} |
|
||||||
{meta ? ( |
|
||||||
<div className='content'> |
|
||||||
<strong>{text}</strong> |
|
||||||
{meta} |
|
||||||
</div> |
|
||||||
) : |
|
||||||
<div className='content'> |
|
||||||
<strong>{text}</strong> |
|
||||||
</div>} |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
}; |
|
||||||
|
|
||||||
// Props.
|
|
||||||
ComposerOptionsDropdownContentItem.propTypes = { |
|
||||||
active: PropTypes.bool, |
|
||||||
name: PropTypes.string, |
|
||||||
onChange: PropTypes.func, |
|
||||||
onClose: PropTypes.func, |
|
||||||
options: PropTypes.shape({ |
|
||||||
icon: PropTypes.string, |
|
||||||
meta: PropTypes.node, |
|
||||||
on: PropTypes.bool, |
|
||||||
text: PropTypes.node, |
|
||||||
}), |
|
||||||
}; |
|
Loading…
Reference in new issue