mirror of
https://github.com/ascribe/onion.git
synced 2024-12-22 17:33:14 +01:00
Implement PropertyCollapsible's functionality into Property
This commit is contained in:
parent
f5a527b251
commit
cb19c46dac
@ -2,61 +2,69 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactAddons from 'react/addons';
|
import ReactAddons from 'react/addons';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
import Panel from 'react-bootstrap/lib/Panel';
|
||||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
|
||||||
|
|
||||||
import AppConstants from '../../constants/application_constants';
|
import AppConstants from '../../constants/application_constants';
|
||||||
|
|
||||||
let Property = React.createClass({
|
import { mergeOptions } from '../../utils/general_utils';
|
||||||
propTypes: {
|
|
||||||
hidden: React.PropTypes.bool,
|
|
||||||
|
|
||||||
autoFocus: React.PropTypes.bool,
|
|
||||||
editable: React.PropTypes.bool,
|
const { bool, element, string, oneOfType, func, object, arrayOf } = React.PropTypes;
|
||||||
|
|
||||||
|
const Property = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
editable: bool,
|
||||||
|
|
||||||
// If we want Form to have a different value for disabled as Property has one for
|
// If we want Form to have a different value for disabled as Property has one for
|
||||||
// editable, we need to set overrideForm to true, as it will then override Form's
|
// editable, we need to set overrideForm to true, as it will then override Form's
|
||||||
// disabled value for individual Properties
|
// disabled value for individual Properties
|
||||||
overrideForm: React.PropTypes.bool,
|
overrideForm: bool,
|
||||||
|
|
||||||
tooltip: React.PropTypes.element,
|
label: string,
|
||||||
label: React.PropTypes.string,
|
value: oneOfType([
|
||||||
value: React.PropTypes.oneOfType([
|
string,
|
||||||
React.PropTypes.string,
|
element
|
||||||
React.PropTypes.element
|
|
||||||
]),
|
]),
|
||||||
footer: React.PropTypes.element,
|
footer: element,
|
||||||
handleChange: React.PropTypes.func,
|
handleChange: func,
|
||||||
ignoreFocus: React.PropTypes.bool,
|
ignoreFocus: bool,
|
||||||
name: React.PropTypes.oneOfType([
|
name: string.isRequired,
|
||||||
React.PropTypes.string,
|
className: string,
|
||||||
React.PropTypes.number
|
|
||||||
]).isRequired,
|
|
||||||
className: React.PropTypes.string,
|
|
||||||
|
|
||||||
onClick: React.PropTypes.func,
|
onClick: func,
|
||||||
onChange: React.PropTypes.func,
|
onChange: func,
|
||||||
onBlur: React.PropTypes.func,
|
onBlur: func,
|
||||||
|
|
||||||
children: React.PropTypes.oneOfType([
|
children: oneOfType([
|
||||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
arrayOf(element),
|
||||||
React.PropTypes.element
|
element
|
||||||
]),
|
]),
|
||||||
style: React.PropTypes.object
|
style: object,
|
||||||
|
expanded: bool,
|
||||||
|
checkboxLabel: string
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
return {
|
return {
|
||||||
editable: true,
|
editable: true,
|
||||||
hidden: false,
|
expanded: true,
|
||||||
className: ''
|
className: ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
|
const { expanded, ignoreFocus, checkboxLabel } = this.props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// We're mirroring expanded here as a state
|
||||||
|
// React's docs do NOT consider this an antipattern as long as it's
|
||||||
|
// not a "source of truth"-duplication
|
||||||
|
expanded,
|
||||||
|
|
||||||
|
// When a checkboxLabel is defined in the props, we want to set
|
||||||
|
// `ignoreFocus` to true
|
||||||
|
ignoreFocus: ignoreFocus || checkboxLabel,
|
||||||
// Please don't confuse initialValue with react's defaultValue.
|
// Please don't confuse initialValue with react's defaultValue.
|
||||||
// initialValue is set by us to ensure that a user can reset a specific
|
// initialValue is set by us to ensure that a user can reset a specific
|
||||||
// property (after editing) to its initial value
|
// property (after editing) to its initial value
|
||||||
@ -67,15 +75,19 @@ let Property = React.createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (this.props.autoFocus) {
|
|
||||||
this.handleFocus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps() {
|
|
||||||
let childInput = this.refs.input;
|
let childInput = this.refs.input;
|
||||||
|
|
||||||
|
// For expanded there are actually two use cases:
|
||||||
|
//
|
||||||
|
// 1. Control its value from the outside completely (do not define `checkboxLabel`)
|
||||||
|
// 2. Let it be controlled from the inside (default value can be set though via `expanded`)
|
||||||
|
//
|
||||||
|
// This handles case 1.
|
||||||
|
if(nextProps.expanded !== this.state.expanded && !this.props.checkboxLabel) {
|
||||||
|
this.setState({ expanded: nextProps.expanded });
|
||||||
|
}
|
||||||
|
|
||||||
// In order to set this.state.value from another component
|
// In order to set this.state.value from another component
|
||||||
// the state of value should only be set if its not undefined and
|
// the state of value should only be set if its not undefined and
|
||||||
// actually references something
|
// actually references something
|
||||||
@ -88,13 +100,13 @@ let Property = React.createClass({
|
|||||||
// from native HTML elements.
|
// from native HTML elements.
|
||||||
// To enable developers to create input elements, they can expose a property called value
|
// To enable developers to create input elements, they can expose a property called value
|
||||||
// in their state that will be picked up by property.js
|
// in their state that will be picked up by property.js
|
||||||
} else if(childInput.state && typeof childInput.state.value !== 'undefined') {
|
} else if(childInput && childInput.state && typeof childInput.state.value !== 'undefined') {
|
||||||
this.setState({
|
this.setState({
|
||||||
value: childInput.state.value
|
value: childInput.state.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.state.initialValue && childInput.props.defaultValue) {
|
if(!this.state.initialValue && childInput && childInput.props.defaultValue) {
|
||||||
this.setState({
|
this.setState({
|
||||||
initialValue: childInput.props.defaultValue
|
initialValue: childInput.props.defaultValue
|
||||||
});
|
});
|
||||||
@ -146,7 +158,7 @@ let Property = React.createClass({
|
|||||||
handleFocus() {
|
handleFocus() {
|
||||||
// if ignoreFocus (bool) is defined, then just ignore focusing on
|
// if ignoreFocus (bool) is defined, then just ignore focusing on
|
||||||
// the property and input
|
// the property and input
|
||||||
if(this.props.ignoreFocus) {
|
if(this.state.ignoreFocus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +210,7 @@ let Property = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getClassName() {
|
getClassName() {
|
||||||
if(this.props.hidden){
|
if(!this.state.expanded && !this.props.checkboxLabel){
|
||||||
return 'is-hidden';
|
return 'is-hidden';
|
||||||
}
|
}
|
||||||
if(!this.props.editable){
|
if(!this.props.editable){
|
||||||
@ -215,63 +227,103 @@ let Property = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderChildren(style) {
|
renderChildren(style) {
|
||||||
return ReactAddons.Children.map(this.props.children, (child) => {
|
// Input's props should only be cloned and propagated down the tree,
|
||||||
return ReactAddons.addons.cloneWithProps(child, {
|
// if the component is actually being shown (!== 'expanded === false')
|
||||||
style,
|
if((this.state.expanded && this.props.checkboxLabel) || !this.props.checkboxLabel) {
|
||||||
onChange: this.handleChange,
|
return ReactAddons.Children.map(this.props.children, (child) => {
|
||||||
onFocus: this.handleFocus,
|
|
||||||
onBlur: this.handleBlur,
|
// Since refs will be overriden by this functions return statement,
|
||||||
disabled: !this.props.editable,
|
// we still want to be able to define refs for nested `Form` or `Property`
|
||||||
ref: 'input',
|
// children, which is why we're upfront simply invoking the callback-ref-
|
||||||
name: this.props.name
|
// function before overriding it.
|
||||||
|
if(typeof child.ref === 'function' && this.refs.input) {
|
||||||
|
child.ref(this.refs.input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
style,
|
||||||
|
onChange: this.handleChange,
|
||||||
|
onFocus: this.handleFocus,
|
||||||
|
onBlur: this.handleBlur,
|
||||||
|
disabled: !this.props.editable,
|
||||||
|
ref: 'input',
|
||||||
|
name: this.props.name
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
getLabelAndErrors() {
|
||||||
const { className, editable, footer, label, tooltip } = this.props;
|
if(this.props.label || this.state.errors) {
|
||||||
const style = Object.assign({}, this.props.style, { cursor: !editable ? 'not-allowed' : null });
|
return (
|
||||||
|
|
||||||
let tooltipEl = tooltip ? <Tooltip>{tooltip}</Tooltip>
|
|
||||||
: <span />;
|
|
||||||
|
|
||||||
let labelEl;
|
|
||||||
if (label || this.state.errors) {
|
|
||||||
labelEl = (
|
|
||||||
<p>
|
<p>
|
||||||
<span className="pull-left">{label}</span>
|
<span className="pull-left">{this.props.label}</span>
|
||||||
<span className="pull-right">{this.state.errors}</span>
|
<span className="pull-right">{this.state.errors}</span>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
let footerEl;
|
handleCheckboxToggle() {
|
||||||
if (footer) {
|
this.setState({expanded: !this.state.expanded});
|
||||||
footerEl = (
|
},
|
||||||
<div className="ascribe-property-footer">
|
|
||||||
{footer}
|
getCheckbox() {
|
||||||
|
const { checkboxLabel } = this.props;
|
||||||
|
|
||||||
|
if(checkboxLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="ascribe-property-collapsible-toggle"
|
||||||
|
onClick={this.handleCheckboxToggle}>
|
||||||
|
<input
|
||||||
|
onChange={this.handleCheckboxToggle}
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.expanded}
|
||||||
|
ref="checkboxCollapsible"/>
|
||||||
|
<span className="checkbox">{' ' + checkboxLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let footer = null;
|
||||||
|
let style = this.props.style ? mergeOptions({}, this.props.style) : {};
|
||||||
|
|
||||||
|
if(this.props.footer){
|
||||||
|
footer = (
|
||||||
|
<div className="ascribe-property-footer">
|
||||||
|
{this.props.footer}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
style.paddingBottom = !this.state.expanded ? 0 : null;
|
||||||
|
style.cursor = !this.props.editable ? 'not-allowed' : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('ascribe-property-wrapper', this.getClassName())}
|
className={'ascribe-property-wrapper ' + this.getClassName()}
|
||||||
onClick={this.handleFocus}
|
onClick={this.handleFocus}
|
||||||
style={style}>
|
style={style}>
|
||||||
<OverlayTrigger
|
{this.getCheckbox()}
|
||||||
delay={500}
|
<Panel
|
||||||
placement="top"
|
collapsible
|
||||||
overlay={tooltipEl}>
|
expanded={this.state.expanded}
|
||||||
<div className={classNames('ascribe-property', this.props.className)}>
|
className="bs-custom-panel">
|
||||||
{labelEl}
|
<div className={'ascribe-property ' + this.props.className}>
|
||||||
|
{this.getLabelAndErrors()}
|
||||||
{this.renderChildren(style)}
|
{this.renderChildren(style)}
|
||||||
{footerEl}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
</OverlayTrigger>
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Property;
|
export default Property;
|
Loading…
Reference in New Issue
Block a user