react-checkbox-tree-enhanced
Version:
A simple, elegant, and enhanced checkbox tree for React.
305 lines (256 loc) • 7.99 kB
JavaScript
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Button from './Button';
import NativeCheckbox from './NativeCheckbox';
import iconsShape from './shapes/iconsShape';
import languageShape from './shapes/languageShape';
const sanitizeValue = value => String(value)
.replace(/[^a-zA-Z0-9 _-]/g, '')
.split(' ')
.join('-');
class TreeNode extends React.Component {
static propTypes = {
checked: PropTypes.number.isRequired,
disabled: PropTypes.bool.isRequired,
expandDisabled: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired,
icons: iconsShape.isRequired,
isLeaf: PropTypes.bool.isRequired,
isParent: PropTypes.bool.isRequired,
label: PropTypes.node.isRequired,
lang: languageShape.isRequired,
optimisticToggle: PropTypes.bool.isRequired,
showNodeIcon: PropTypes.bool.isRequired,
treeId: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onCheck: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
children: PropTypes.node,
className: PropTypes.string,
expandOnClick: PropTypes.bool,
icon: PropTypes.node,
showCheckbox: PropTypes.bool,
title: PropTypes.string,
onClick: PropTypes.func,
};
static defaultProps = {
children: null,
className: null,
expandOnClick: false,
icon: null,
showCheckbox: true,
title: null,
onClick: () => {},
};
constructor(props) {
super(props);
this.onCheck = this.onCheck.bind(this);
this.onClick = this.onClick.bind(this);
this.onExpand = this.onExpand.bind(this);
}
onCheck() {
const { value, onCheck } = this.props;
onCheck({ value, checked: this.getCheckState({ toggle: true }) });
}
onClick() {
const {
expandOnClick, isParent, value, onClick,
} = this.props;
// Auto expand if enabled
if (isParent && expandOnClick) {
this.onExpand();
}
onClick({ value, checked: this.getCheckState({ toggle: false }) });
}
onExpand() {
const { expanded, value, onExpand } = this.props;
onExpand({ value, expanded: !expanded });
}
getCheckState({ toggle }) {
const { checked, optimisticToggle } = this.props;
// Toggle off state to checked
if (checked === 0 && toggle) {
return true;
}
// Node is already checked and we are not toggling
if (checked === 1 && !toggle) {
return true;
}
// Get/toggle partial state based on cascade model
if (checked === 2) {
return optimisticToggle;
}
return false;
}
renderCollapseButton() {
const { expandDisabled, isLeaf, lang } = this.props;
if (isLeaf) {
return (
<span className='rct-collapse'>
<span className='rct-icon' />
</span>
);
}
return (
<Button
className='rct-collapse rct-collapse-btn'
disabled={expandDisabled}
title={lang.toggle}
onClick={this.onExpand}
>
{this.renderCollapseIcon()}
</Button>
);
}
renderCollapseIcon() {
const {
expanded,
icons: { expandClose, expandOpen },
} = this.props;
if (!expanded) {
return expandClose;
}
return expandOpen;
}
renderCheckboxIcon() {
const {
checked,
icons: { uncheck, check, halfCheck },
} = this.props;
if (checked === 0) {
return uncheck;
}
if (checked === 1) {
return check;
}
return halfCheck;
}
renderNodeIcon() {
const {
expanded,
icon,
icons: { leaf, parentClose, parentOpen },
isLeaf,
} = this.props;
if (icon !== null) {
return icon;
}
if (isLeaf) {
return leaf;
}
if (!expanded) {
return parentClose;
}
return parentOpen;
}
renderBareLabel(children) {
const { onClick } = this.props;
const clickable = onClick !== null;
return (
<span className='rct-bare-label'>
{clickable ? (
<span
className='rct-node-clickable'
onClick={this.onClick}
onKeyPress={this.onClick}
role='button'
tabIndex={0}
>
{children}
</span>
) : (
children
)}
</span>
);
}
renderCheckboxLabel(children) {
const {
checked, disabled, treeId, value, onClick,
} = this.props;
const clickable = onClick !== null;
const inputId = `${treeId}-${String(value)
.split(' ')
.join('_')}`;
const render = [
<label key={0} htmlFor={inputId}>
<NativeCheckbox
checked={checked === 1}
disabled={disabled}
id={inputId}
indeterminate={checked === 2}
onClick={this.onCheck}
onChange={() => {}}
/>
<span className='rct-checkbox'>{this.renderCheckboxIcon()}</span>
{!clickable ? children : null}
</label>,
];
if (clickable) {
render.push(
<span
key={1}
className='rct-node-clickable'
onClick={this.onClick}
onKeyPress={this.onClick}
role='link'
tabIndex={0}
>
{children}
</span>,
);
}
return render;
}
renderLabel() {
const { label, showCheckbox, showNodeIcon } = this.props;
const labelChildren = [
showNodeIcon ? (
<span key={0} className='rct-node-icon'>
{this.renderNodeIcon()}
</span>
) : null,
<span key={1} className='rct-title'>
{label}
</span>,
];
if (!showCheckbox) {
return this.renderBareLabel(labelChildren);
}
return this.renderCheckboxLabel(labelChildren);
}
renderChildren() {
if (!this.props.expanded) {
return null;
}
return this.props.children;
}
render() {
const {
className, disabled, expanded, isLeaf, title, treeId, value,
} = this.props;
const nodeClass = classNames(
{
'rct-node': true,
'rct-node-leaf': isLeaf,
'rct-node-parent': !isLeaf,
'rct-node-expanded': !isLeaf && expanded,
'rct-node-collapsed': !isLeaf && !expanded,
'rct-disabled': disabled,
},
className,
);
const textId = `${treeId}-text-${sanitizeValue(value)}`;
return (
<li className={nodeClass}>
<span id={textId} className='rct-text' title={title}>
{this.renderCollapseButton()}
{this.renderLabel()}
</span>
{this.renderChildren()}
</li>
);
}
}
export default TreeNode;