Skip to content

Commit 39be018

Browse files
committed
move cell edit logic to react-bootstrap-table2-editor
1 parent 6913434 commit 39be018

File tree

17 files changed

+267
-246
lines changed

17 files changed

+267
-246
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "react-bootstrap-table2-editor",
3+
"version": "0.0.1",
4+
"description": "it's the editor addon for react-bootstrap-table2",
5+
"main": "src/index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "ISC"
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const TIME_TO_CLOSE_MESSAGE = 3000;
2+
export const DELAY_FOR_DBCLICK = 200;
3+
export const CLICK_TO_CELL_EDIT = 'click';
4+
export const DBCLICK_TO_CELL_EDIT = 'dbclick';
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/* eslint react/prop-types: 0 */
2+
/* eslint no-return-assign: 0 */
3+
/* eslint class-methods-use-this: 0 */
4+
/* eslint jsx-a11y/no-noninteractive-element-interactions: 0 */
5+
import React, { Component } from 'react';
6+
import cs from 'classnames';
7+
import PropTypes from 'prop-types';
8+
9+
import TextEditor from './text-editor';
10+
import EditorIndicator from './editor-indicator';
11+
import { TIME_TO_CLOSE_MESSAGE } from './const';
12+
13+
export default _ =>
14+
class EditingCell extends Component {
15+
static propTypes = {
16+
row: PropTypes.object.isRequired,
17+
column: PropTypes.object.isRequired,
18+
onUpdate: PropTypes.func.isRequired,
19+
onEscape: PropTypes.func.isRequired,
20+
timeToCloseMessage: PropTypes.number,
21+
className: PropTypes.string,
22+
style: PropTypes.object
23+
}
24+
25+
static defaultProps = {
26+
timeToCloseMessage: TIME_TO_CLOSE_MESSAGE,
27+
className: null,
28+
style: {}
29+
}
30+
31+
constructor(props) {
32+
super(props);
33+
this.indicatorTimer = null;
34+
this.clearTimer = this.clearTimer.bind(this);
35+
this.handleBlur = this.handleBlur.bind(this);
36+
this.handleClick = this.handleClick.bind(this);
37+
this.handleKeyDown = this.handleKeyDown.bind(this);
38+
this.beforeComplete = this.beforeComplete.bind(this);
39+
this.state = {
40+
invalidMessage: null
41+
};
42+
}
43+
44+
componentWillReceiveProps({ message }) {
45+
if (_.isDefined(message)) {
46+
this.createTimer();
47+
this.setState(() => ({
48+
invalidMessage: message
49+
}));
50+
}
51+
}
52+
53+
componentWillUnmount() {
54+
this.clearTimer();
55+
}
56+
57+
clearTimer() {
58+
if (this.indicatorTimer) {
59+
clearTimeout(this.indicatorTimer);
60+
}
61+
}
62+
63+
createTimer() {
64+
this.clearTimer();
65+
const { timeToCloseMessage, onErrorMessageDisappear } = this.props;
66+
this.indicatorTimer = _.sleep(() => {
67+
this.setState(() => ({
68+
invalidMessage: null
69+
}));
70+
if (_.isFunction(onErrorMessageDisappear)) onErrorMessageDisappear();
71+
}, timeToCloseMessage);
72+
}
73+
74+
beforeComplete(row, column, newValue) {
75+
const { onUpdate } = this.props;
76+
if (_.isFunction(column.validator)) {
77+
const validateForm = column.validator(newValue, row, column);
78+
if (_.isObject(validateForm) && !validateForm.valid) {
79+
this.setState(() => ({
80+
invalidMessage: validateForm.message
81+
}));
82+
this.createTimer();
83+
return;
84+
}
85+
}
86+
onUpdate(row, column, newValue);
87+
}
88+
89+
handleBlur() {
90+
const { onEscape, blurToSave, row, column } = this.props;
91+
if (blurToSave) {
92+
const value = this.editor.text.value;
93+
if (!_.isDefined(value)) {
94+
// TODO: for other custom or embed editor
95+
}
96+
this.beforeComplete(row, column, value);
97+
} else {
98+
onEscape();
99+
}
100+
}
101+
102+
handleKeyDown(e) {
103+
const { onEscape, row, column } = this.props;
104+
if (e.keyCode === 27) { // ESC
105+
onEscape();
106+
} else if (e.keyCode === 13) { // ENTER
107+
const value = e.currentTarget.value;
108+
if (!_.isDefined(value)) {
109+
// TODO: for other custom or embed editor
110+
}
111+
this.beforeComplete(row, column, value);
112+
}
113+
}
114+
115+
handleClick(e) {
116+
if (e.target.tagName !== 'TD') {
117+
// To avoid the row selection event be triggered,
118+
// When user define selectRow.clickToSelect and selectRow.clickToEdit
119+
// We shouldn't trigger selection event even if user click on the cell editor(input)
120+
e.stopPropagation();
121+
}
122+
}
123+
124+
render() {
125+
const { invalidMessage } = this.state;
126+
const { row, column, className, style } = this.props;
127+
const { dataField } = column;
128+
129+
const value = _.get(row, dataField);
130+
const editorAttrs = {
131+
onKeyDown: this.handleKeyDown,
132+
onBlur: this.handleBlur
133+
};
134+
135+
const hasError = _.isDefined(invalidMessage);
136+
const editorClass = hasError ? cs('animated', 'shake') : null;
137+
return (
138+
<td
139+
className={ cs('react-bootstrap-table-editing-cell', className) }
140+
style={ style }
141+
onClick={ this.handleClick }
142+
>
143+
<TextEditor
144+
ref={ node => this.editor = node }
145+
defaultValue={ value }
146+
className={ editorClass }
147+
{ ...editorAttrs }
148+
/>
149+
{ hasError ? <EditorIndicator invalidMessage={ invalidMessage } /> : null }
150+
</td>
151+
);
152+
}
153+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import wrapperFactory from './wrapper';
2+
import editingCellFactory from './editing-cell';
3+
import {
4+
CLICK_TO_CELL_EDIT,
5+
DBCLICK_TO_CELL_EDIT,
6+
DELAY_FOR_DBCLICK
7+
} from './const';
8+
9+
export default (options = {}) => ({
10+
wrapperFactory,
11+
editingCellFactory,
12+
CLICK_TO_CELL_EDIT,
13+
DBCLICK_TO_CELL_EDIT,
14+
DELAY_FOR_DBCLICK,
15+
options
16+
});

packages/react-bootstrap-table2/src/cell-edit/wrapper.js renamed to packages/react-bootstrap-table2-editor/src/wrapper.js

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
/* eslint react/prop-types: 0 */
22
import React, { Component } from 'react';
3-
import _ from '../utils';
4-
import remoteResolver from '../props-resolver/remote-resolver';
3+
import PropTypes from 'prop-types';
54

6-
export default Base =>
5+
import { CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT } from './const';
6+
7+
export default (
8+
Base,
9+
{ _, remoteResolver }
10+
) =>
711
class CellEditWrapper extends remoteResolver(Component) {
12+
static propTypes = {
13+
options: PropTypes.shape({
14+
mode: PropTypes.oneOf([CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT]).isRequired,
15+
onErrorMessageDisappear: PropTypes.func,
16+
blurToSave: PropTypes.bool,
17+
beforeSaveCell: PropTypes.func,
18+
afterSaveCell: PropTypes.func,
19+
nonEditableRows: PropTypes.func,
20+
timeToCloseMessage: PropTypes.number,
21+
errorMessage: PropTypes.string
22+
})
23+
}
24+
825
constructor(props) {
926
super(props);
1027
this.startEditing = this.startEditing.bind(this);
@@ -21,10 +38,10 @@ export default Base =>
2138

2239
componentWillReceiveProps(nextProps) {
2340
if (nextProps.cellEdit && this.isRemoteCellEdit()) {
24-
if (nextProps.cellEdit.errorMessage) {
41+
if (nextProps.cellEdit.options.errorMessage) {
2542
this.setState(() => ({
2643
isDataChanged: false,
27-
message: nextProps.cellEdit.errorMessage
44+
message: nextProps.cellEdit.options.errorMessage
2845
}));
2946
} else {
3047
this.setState(() => ({
@@ -41,7 +58,7 @@ export default Base =>
4158

4259
handleCellUpdate(row, column, newValue) {
4360
const { keyField, cellEdit, store } = this.props;
44-
const { beforeSaveCell, afterSaveCell } = cellEdit;
61+
const { beforeSaveCell, afterSaveCell } = cellEdit.options;
4562
const oldValue = _.get(row, column.dataField);
4663
const rowId = _.get(row, keyField);
4764
if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column);
@@ -84,16 +101,31 @@ export default Base =>
84101
}
85102

86103
render() {
87-
const { isDataChanged, ...rest } = this.state;
104+
const { isDataChanged, ...stateRest } = this.state;
105+
const {
106+
cellEdit: {
107+
options: { nonEditableRows, ...optionsRest },
108+
editingCellFactory,
109+
...cellEditRest
110+
}
111+
} = this.props;
112+
const newCellEdit = {
113+
...optionsRest,
114+
...cellEditRest,
115+
...stateRest,
116+
nonEditableRows: _.isDefined(nonEditableRows) ? nonEditableRows() : [],
117+
EditingCell: editingCellFactory(_),
118+
onStart: this.startEditing,
119+
onEscape: this.escapeEditing,
120+
onUpdate: this.handleCellUpdate
121+
};
122+
88123
return (
89124
<Base
90125
{ ...this.props }
91-
isDataChanged={ isDataChanged }
92126
data={ this.props.store.data }
93-
onCellUpdate={ this.handleCellUpdate }
94-
onStartEditing={ this.startEditing }
95-
onEscapeEditing={ this.escapeEditing }
96-
currEditCell={ { ...rest } }
127+
isDataChanged={ isDataChanged }
128+
cellEdit={ newCellEdit }
97129
/>
98130
);
99131
}

packages/react-bootstrap-table2-example/.storybook/webpack.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const sourcePath = path.join(__dirname, '../../react-bootstrap-table2/src');
44
const paginationSourcePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/src');
55
const overlaySourcePath = path.join(__dirname, '../../react-bootstrap-table2-overlay/src');
66
const filterSourcePath = path.join(__dirname, '../../react-bootstrap-table2-filter/src');
7+
const editorSourcePath = path.join(__dirname, '../../react-bootstrap-table2-editor/src');
78
const sourceStylePath = path.join(__dirname, '../../react-bootstrap-table2/style');
89
const paginationStylePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/style');
910
const storyPath = path.join(__dirname, '../stories');
@@ -27,7 +28,7 @@ const loaders = [{
2728
test: /\.js?$/,
2829
use: ['babel-loader'],
2930
exclude: /node_modules/,
30-
include: [sourcePath, paginationSourcePath, overlaySourcePath, filterSourcePath, storyPath],
31+
include: [sourcePath, paginationSourcePath, overlaySourcePath, filterSourcePath, editorSourcePath, storyPath],
3132
}, {
3233
test: /\.css$/,
3334
use: ['style-loader', 'css-loader'],

packages/react-bootstrap-table2-example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dependencies": {
1919
"bootstrap": "^3.3.7",
2020
"react-bootstrap-table2": "0.0.1",
21+
"react-bootstrap-table2-editor": "0.0.1",
2122
"react-bootstrap-table2-paginator": "0.0.1",
2223
"react-bootstrap-table2-overlay": "0.0.1",
2324
"react-bootstrap-table2-filter": "0.0.1"

packages/react-bootstrap-table2/src/body.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ const Body = (props) => {
3737
const indication = _.isFunction(noDataIndication) ? noDataIndication() : noDataIndication;
3838
content = <RowSection content={ indication } colSpan={ visibleColumnSize } />;
3939
} else {
40+
const nonEditableRows = cellEdit.nonEditableRows || [];
4041
content = data.map((row, index) => {
4142
const key = _.get(row, keyField);
42-
const editable = !(cellEdit.mode !== Const.UNABLE_TO_CELL_EDIT &&
43-
cellEdit.nonEditableRows.indexOf(key) > -1);
43+
const editable = !(nonEditableRows.length > 0 && nonEditableRows.indexOf(key) > -1);
4444

4545
const selected = selectRow.mode !== Const.ROW_SELECT_DISABLED
4646
? selectedRowKeys.includes(key)

0 commit comments

Comments
 (0)