move cell edit logic to react-bootstrap-table2-editor

This commit is contained in:
AllenFang
2018-01-06 15:53:20 +08:00
parent 6913434714
commit 39be018327
17 changed files with 267 additions and 246 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "react-bootstrap-table2-editor",
"version": "0.0.1",
"description": "it's the editor addon for react-bootstrap-table2",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,4 @@
export const TIME_TO_CLOSE_MESSAGE = 3000;
export const DELAY_FOR_DBCLICK = 200;
export const CLICK_TO_CELL_EDIT = 'click';
export const DBCLICK_TO_CELL_EDIT = 'dbclick';

View File

@@ -0,0 +1,153 @@
/* eslint react/prop-types: 0 */
/* eslint no-return-assign: 0 */
/* eslint class-methods-use-this: 0 */
/* eslint jsx-a11y/no-noninteractive-element-interactions: 0 */
import React, { Component } from 'react';
import cs from 'classnames';
import PropTypes from 'prop-types';
import TextEditor from './text-editor';
import EditorIndicator from './editor-indicator';
import { TIME_TO_CLOSE_MESSAGE } from './const';
export default _ =>
class EditingCell extends Component {
static propTypes = {
row: PropTypes.object.isRequired,
column: PropTypes.object.isRequired,
onUpdate: PropTypes.func.isRequired,
onEscape: PropTypes.func.isRequired,
timeToCloseMessage: PropTypes.number,
className: PropTypes.string,
style: PropTypes.object
}
static defaultProps = {
timeToCloseMessage: TIME_TO_CLOSE_MESSAGE,
className: null,
style: {}
}
constructor(props) {
super(props);
this.indicatorTimer = null;
this.clearTimer = this.clearTimer.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.beforeComplete = this.beforeComplete.bind(this);
this.state = {
invalidMessage: null
};
}
componentWillReceiveProps({ message }) {
if (_.isDefined(message)) {
this.createTimer();
this.setState(() => ({
invalidMessage: message
}));
}
}
componentWillUnmount() {
this.clearTimer();
}
clearTimer() {
if (this.indicatorTimer) {
clearTimeout(this.indicatorTimer);
}
}
createTimer() {
this.clearTimer();
const { timeToCloseMessage, onErrorMessageDisappear } = this.props;
this.indicatorTimer = _.sleep(() => {
this.setState(() => ({
invalidMessage: null
}));
if (_.isFunction(onErrorMessageDisappear)) onErrorMessageDisappear();
}, timeToCloseMessage);
}
beforeComplete(row, column, newValue) {
const { onUpdate } = this.props;
if (_.isFunction(column.validator)) {
const validateForm = column.validator(newValue, row, column);
if (_.isObject(validateForm) && !validateForm.valid) {
this.setState(() => ({
invalidMessage: validateForm.message
}));
this.createTimer();
return;
}
}
onUpdate(row, column, newValue);
}
handleBlur() {
const { onEscape, blurToSave, row, column } = this.props;
if (blurToSave) {
const value = this.editor.text.value;
if (!_.isDefined(value)) {
// TODO: for other custom or embed editor
}
this.beforeComplete(row, column, value);
} else {
onEscape();
}
}
handleKeyDown(e) {
const { onEscape, row, column } = this.props;
if (e.keyCode === 27) { // ESC
onEscape();
} else if (e.keyCode === 13) { // ENTER
const value = e.currentTarget.value;
if (!_.isDefined(value)) {
// TODO: for other custom or embed editor
}
this.beforeComplete(row, column, value);
}
}
handleClick(e) {
if (e.target.tagName !== 'TD') {
// To avoid the row selection event be triggered,
// When user define selectRow.clickToSelect and selectRow.clickToEdit
// We shouldn't trigger selection event even if user click on the cell editor(input)
e.stopPropagation();
}
}
render() {
const { invalidMessage } = this.state;
const { row, column, className, style } = this.props;
const { dataField } = column;
const value = _.get(row, dataField);
const editorAttrs = {
onKeyDown: this.handleKeyDown,
onBlur: this.handleBlur
};
const hasError = _.isDefined(invalidMessage);
const editorClass = hasError ? cs('animated', 'shake') : null;
return (
<td
className={ cs('react-bootstrap-table-editing-cell', className) }
style={ style }
onClick={ this.handleClick }
>
<TextEditor
ref={ node => this.editor = node }
defaultValue={ value }
className={ editorClass }
{ ...editorAttrs }
/>
{ hasError ? <EditorIndicator invalidMessage={ invalidMessage } /> : null }
</td>
);
}
};

View File

@@ -0,0 +1,16 @@
import wrapperFactory from './wrapper';
import editingCellFactory from './editing-cell';
import {
CLICK_TO_CELL_EDIT,
DBCLICK_TO_CELL_EDIT,
DELAY_FOR_DBCLICK
} from './const';
export default (options = {}) => ({
wrapperFactory,
editingCellFactory,
CLICK_TO_CELL_EDIT,
DBCLICK_TO_CELL_EDIT,
DELAY_FOR_DBCLICK,
options
});

View File

@@ -1,10 +1,27 @@
/* eslint react/prop-types: 0 */
import React, { Component } from 'react';
import _ from '../utils';
import remoteResolver from '../props-resolver/remote-resolver';
import PropTypes from 'prop-types';
export default Base =>
import { CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT } from './const';
export default (
Base,
{ _, remoteResolver }
) =>
class CellEditWrapper extends remoteResolver(Component) {
static propTypes = {
options: PropTypes.shape({
mode: PropTypes.oneOf([CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT]).isRequired,
onErrorMessageDisappear: PropTypes.func,
blurToSave: PropTypes.bool,
beforeSaveCell: PropTypes.func,
afterSaveCell: PropTypes.func,
nonEditableRows: PropTypes.func,
timeToCloseMessage: PropTypes.number,
errorMessage: PropTypes.string
})
}
constructor(props) {
super(props);
this.startEditing = this.startEditing.bind(this);
@@ -21,10 +38,10 @@ export default Base =>
componentWillReceiveProps(nextProps) {
if (nextProps.cellEdit && this.isRemoteCellEdit()) {
if (nextProps.cellEdit.errorMessage) {
if (nextProps.cellEdit.options.errorMessage) {
this.setState(() => ({
isDataChanged: false,
message: nextProps.cellEdit.errorMessage
message: nextProps.cellEdit.options.errorMessage
}));
} else {
this.setState(() => ({
@@ -41,7 +58,7 @@ export default Base =>
handleCellUpdate(row, column, newValue) {
const { keyField, cellEdit, store } = this.props;
const { beforeSaveCell, afterSaveCell } = cellEdit;
const { beforeSaveCell, afterSaveCell } = cellEdit.options;
const oldValue = _.get(row, column.dataField);
const rowId = _.get(row, keyField);
if (_.isFunction(beforeSaveCell)) beforeSaveCell(oldValue, newValue, row, column);
@@ -84,16 +101,31 @@ export default Base =>
}
render() {
const { isDataChanged, ...rest } = this.state;
const { isDataChanged, ...stateRest } = this.state;
const {
cellEdit: {
options: { nonEditableRows, ...optionsRest },
editingCellFactory,
...cellEditRest
}
} = this.props;
const newCellEdit = {
...optionsRest,
...cellEditRest,
...stateRest,
nonEditableRows: _.isDefined(nonEditableRows) ? nonEditableRows() : [],
EditingCell: editingCellFactory(_),
onStart: this.startEditing,
onEscape: this.escapeEditing,
onUpdate: this.handleCellUpdate
};
return (
<Base
{ ...this.props }
isDataChanged={ isDataChanged }
data={ this.props.store.data }
onCellUpdate={ this.handleCellUpdate }
onStartEditing={ this.startEditing }
onEscapeEditing={ this.escapeEditing }
currEditCell={ { ...rest } }
isDataChanged={ isDataChanged }
cellEdit={ newCellEdit }
/>
);
}

View File

@@ -4,6 +4,7 @@ const sourcePath = path.join(__dirname, '../../react-bootstrap-table2/src');
const paginationSourcePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/src');
const overlaySourcePath = path.join(__dirname, '../../react-bootstrap-table2-overlay/src');
const filterSourcePath = path.join(__dirname, '../../react-bootstrap-table2-filter/src');
const editorSourcePath = path.join(__dirname, '../../react-bootstrap-table2-editor/src');
const sourceStylePath = path.join(__dirname, '../../react-bootstrap-table2/style');
const paginationStylePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/style');
const storyPath = path.join(__dirname, '../stories');
@@ -27,7 +28,7 @@ const loaders = [{
test: /\.js?$/,
use: ['babel-loader'],
exclude: /node_modules/,
include: [sourcePath, paginationSourcePath, overlaySourcePath, filterSourcePath, storyPath],
include: [sourcePath, paginationSourcePath, overlaySourcePath, filterSourcePath, editorSourcePath, storyPath],
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader'],

View File

@@ -18,6 +18,7 @@
"dependencies": {
"bootstrap": "^3.3.7",
"react-bootstrap-table2": "0.0.1",
"react-bootstrap-table2-editor": "0.0.1",
"react-bootstrap-table2-paginator": "0.0.1",
"react-bootstrap-table2-overlay": "0.0.1",
"react-bootstrap-table2-filter": "0.0.1"

View File

@@ -37,10 +37,10 @@ const Body = (props) => {
const indication = _.isFunction(noDataIndication) ? noDataIndication() : noDataIndication;
content = <RowSection content={ indication } colSpan={ visibleColumnSize } />;
} else {
const nonEditableRows = cellEdit.nonEditableRows || [];
content = data.map((row, index) => {
const key = _.get(row, keyField);
const editable = !(cellEdit.mode !== Const.UNABLE_TO_CELL_EDIT &&
cellEdit.nonEditableRows.indexOf(key) > -1);
const editable = !(nonEditableRows.length > 0 && nonEditableRows.indexOf(key) > -1);
const selected = selectRow.mode !== Const.ROW_SELECT_DISABLED
? selectedRowKeys.includes(key)

View File

@@ -60,13 +60,6 @@ class BootstrapTable extends PropsBaseResolver(Component) {
'table-condensed': condensed
});
const cellEditInfo = this.resolveCellEditProps({
onStart: this.props.onStartEditing,
onEscape: this.props.onEscapeEditing,
onUpdate: this.props.onCellUpdate,
currEditCell: this.props.currEditCell
});
const cellSelectionInfo = this.resolveSelectRowProps({
onRowSelect: this.props.onRowSelect
});
@@ -96,7 +89,7 @@ class BootstrapTable extends PropsBaseResolver(Component) {
isEmpty={ this.isEmpty() }
visibleColumnSize={ this.visibleColumnSize() }
noDataIndication={ noDataIndication }
cellEdit={ cellEditInfo }
cellEdit={ this.props.cellEdit || {} }
selectRow={ cellSelectionInfo }
selectedRowKeys={ store.selected }
rowStyle={ rowStyle }
@@ -128,24 +121,7 @@ BootstrapTable.propTypes = {
]),
pagination: PropTypes.object,
filter: PropTypes.object,
cellEdit: PropTypes.shape({
mode: PropTypes.oneOf([Const.CLICK_TO_CELL_EDIT, Const.DBCLICK_TO_CELL_EDIT]).isRequired,
onErrorMessageDisappear: PropTypes.func,
blurToSave: PropTypes.bool,
beforeSaveCell: PropTypes.func,
afterSaveCell: PropTypes.func,
nonEditableRows: PropTypes.func,
timeToCloseMessage: PropTypes.number,
errorMessage: PropTypes.string
}),
onCellUpdate: PropTypes.func,
onStartEditing: PropTypes.func,
onEscapeEditing: PropTypes.func,
currEditCell: PropTypes.shape({
ridx: PropTypes.number,
cidx: PropTypes.number,
message: PropTypes.string
}),
cellEdit: PropTypes.object,
selectRow: PropTypes.shape({
mode: PropTypes.oneOf([Const.ROW_SELECT_SINGLE, Const.ROW_SELECT_MULTIPLE]).isRequired,
clickToSelect: PropTypes.bool,

View File

@@ -1,156 +0,0 @@
/* eslint arrow-body-style: 0 */
/* eslint react/prop-types: 0 */
/* eslint no-return-assign: 0 */
/* eslint class-methods-use-this: 0 */
/* eslint jsx-a11y/no-noninteractive-element-interactions: 0 */
import React, { Component } from 'react';
import cs from 'classnames';
import PropTypes from 'prop-types';
import _ from '../utils';
import Const from '../const';
import TextEditor from './text-editor';
import EditorIndicator from './editor-indicator';
class EditingCell extends Component {
constructor(props) {
super(props);
this.indicatorTimer = null;
this.clearTimer = this.clearTimer.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.beforeComplete = this.beforeComplete.bind(this);
this.state = {
invalidMessage: null
};
}
componentWillReceiveProps({ message }) {
if (_.isDefined(message)) {
this.createTimer();
this.setState(() => {
return { invalidMessage: message };
});
}
}
componentWillUnmount() {
this.clearTimer();
}
clearTimer() {
if (this.indicatorTimer) {
clearTimeout(this.indicatorTimer);
}
}
createTimer() {
this.clearTimer();
const { timeToCloseMessage, onErrorMessageDisappear } = this.props;
this.indicatorTimer = _.sleep(() => {
this.setState(() => {
return { invalidMessage: null };
});
if (_.isFunction(onErrorMessageDisappear)) onErrorMessageDisappear();
}, timeToCloseMessage);
}
beforeComplete(row, column, newValue) {
const { onUpdate } = this.props;
if (_.isFunction(column.validator)) {
const validateForm = column.validator(newValue, row, column);
if (_.isObject(validateForm) && !validateForm.valid) {
this.setState(() => {
return { invalidMessage: validateForm.message };
});
this.createTimer();
return;
}
}
onUpdate(row, column, newValue);
}
handleBlur() {
const { onEscape, blurToSave, row, column } = this.props;
if (blurToSave) {
const value = this.editor.text.value;
if (!_.isDefined(value)) {
// TODO: for other custom or embed editor
}
this.beforeComplete(row, column, value);
} else {
onEscape();
}
}
handleKeyDown(e) {
const { onEscape, row, column } = this.props;
if (e.keyCode === 27) { // ESC
onEscape();
} else if (e.keyCode === 13) { // ENTER
const value = e.currentTarget.value;
if (!_.isDefined(value)) {
// TODO: for other custom or embed editor
}
this.beforeComplete(row, column, value);
}
}
handleClick(e) {
if (e.target.tagName !== 'TD') {
// To avoid the row selection event be triggered,
// When user define selectRow.clickToSelect and selectRow.clickToEdit
// We shouldn't trigger selection event even if user click on the cell editor(input)
e.stopPropagation();
}
}
render() {
const { invalidMessage } = this.state;
const { row, column, className, style } = this.props;
const { dataField } = column;
const value = _.get(row, dataField);
const editorAttrs = {
onKeyDown: this.handleKeyDown,
onBlur: this.handleBlur
};
const hasError = _.isDefined(invalidMessage);
const editorClass = hasError ? cs('animated', 'shake') : null;
return (
<td
className={ cs('react-bootstrap-table-editing-cell', className) }
style={ style }
onClick={ this.handleClick }
>
<TextEditor
ref={ node => this.editor = node }
defaultValue={ value }
className={ editorClass }
{ ...editorAttrs }
/>
{ hasError ? <EditorIndicator invalidMessage={ invalidMessage } /> : null }
</td>
);
}
}
EditingCell.propTypes = {
row: PropTypes.object.isRequired,
column: PropTypes.object.isRequired,
onUpdate: PropTypes.func.isRequired,
onEscape: PropTypes.func.isRequired,
timeToCloseMessage: PropTypes.number,
className: PropTypes.string,
style: PropTypes.object
};
EditingCell.defaultProps = {
timeToCloseMessage: Const.TIME_TO_CLOSE_MESSAGE,
className: null,
style: {}
};
export default EditingCell;

View File

@@ -2,7 +2,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Const from './const';
import _ from './utils';
class Cell extends Component {
@@ -12,18 +11,20 @@ class Cell extends Component {
}
handleEditingCell(e) {
const { editMode, column, onStart, rowIndex, columnIndex } = this.props;
const { column, onStart, rowIndex, columnIndex, clickToEdit, dbclickToEdit } = this.props;
const { events } = column;
if (events) {
if (editMode === Const.CLICK_TO_CELL_EDIT) {
if (clickToEdit) {
const customClick = events.onClick;
if (_.isFunction(customClick)) customClick(e);
} else {
} else if (dbclickToEdit) {
const customDbClick = events.onDoubleClick;
if (_.isFunction(customDbClick)) customDbClick(e);
}
}
onStart(rowIndex, columnIndex);
if (onStart) {
onStart(rowIndex, columnIndex);
}
}
render() {
@@ -32,8 +33,9 @@ class Cell extends Component {
rowIndex,
column,
columnIndex,
editMode,
editable
editable,
clickToEdit,
dbclickToEdit
} = this.props;
const {
dataField,
@@ -85,12 +87,10 @@ class Cell extends Component {
if (cellClasses) cellAttrs.className = cellClasses;
if (!_.isEmptyObject(cellStyle)) cellAttrs.style = cellStyle;
if (editable && editMode !== Const.UNABLE_TO_CELL_EDIT) {
if (editMode === Const.CLICK_TO_CELL_EDIT) {
cellAttrs.onClick = this.handleEditingCell;
} else {
cellAttrs.onDoubleClick = this.handleEditingCell;
}
if (clickToEdit && editable) {
cellAttrs.onClick = this.handleEditingCell;
} else if (dbclickToEdit && editable) {
cellAttrs.onDoubleClick = this.handleEditingCell;
}
return (
<td { ...cellAttrs }>{ content }</td>

View File

@@ -1,15 +1,10 @@
export default {
SORT_ASC: 'asc',
SORT_DESC: 'desc',
UNABLE_TO_CELL_EDIT: 'none',
CLICK_TO_CELL_EDIT: 'click',
DBCLICK_TO_CELL_EDIT: 'dbclick',
TIME_TO_CLOSE_MESSAGE: 3000,
ROW_SELECT_SINGLE: 'radio',
ROW_SELECT_MULTIPLE: 'checkbox',
ROW_SELECT_DISABLED: 'ROW_SELECT_DISABLED',
CHECKBOX_STATUS_CHECKED: 'checked',
CHECKBOX_STATUS_INDETERMINATE: 'indeterminate',
CHECKBOX_STATUS_UNCHECKED: 'unchecked',
DELAY_FOR_DBCLICK: 200
CHECKBOX_STATUS_UNCHECKED: 'unchecked'
};

View File

@@ -3,7 +3,6 @@
import React, { Component } from 'react';
import Store from './store';
import withSort from './sort/wrapper';
import withCellEdit from './cell-edit/wrapper';
import withSelection from './row-selection/wrapper';
import remoteResolver from './props-resolver/remote-resolver';
@@ -45,7 +44,11 @@ const withDataStore = Base =>
}
if (cellEdit) {
this.BaseComponent = withCellEdit(this.BaseComponent);
const { wrapperFactory } = cellEdit;
this.BaseComponent = wrapperFactory(this.BaseComponent, {
_,
remoteResolver
});
}
if (selectRow) {

View File

@@ -18,28 +18,6 @@ export default ExtendBase =>
return this.props.data.length === 0;
}
resolveCellEditProps(options = { currEditCell: null }) {
const { cellEdit } = this.props;
const nonEditableRows =
(cellEdit && _.isFunction(cellEdit.nonEditableRows)) ? cellEdit.nonEditableRows() : [];
const cellEditInfo = {
...options.currEditCell,
nonEditableRows
};
if (_.isDefined(cellEdit)) {
return {
...cellEdit,
...cellEditInfo,
...options
};
}
return {
mode: Const.UNABLE_TO_CELL_EDIT,
...cellEditInfo
};
}
/**
* props resolver for cell selection
* @param {Object} options - addtional options like callback which are about to merge into props

View File

@@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
import _ from './utils';
import Cell from './cell';
import SelectionCell from './row-selection/selection-cell';
import EditingCell from './cell-edit/editing-cell';
import Const from './const';
class Row extends Component {
@@ -26,7 +25,11 @@ class Row extends Component {
onRowSelect,
clickToEdit
},
cellEdit: { mode },
cellEdit: {
mode,
DBCLICK_TO_CELL_EDIT,
DELAY_FOR_DBCLICK
},
attrs
} = this.props;
@@ -40,14 +43,14 @@ class Row extends Component {
}
};
if (mode === Const.DBCLICK_TO_CELL_EDIT && clickToEdit) {
if (mode === DBCLICK_TO_CELL_EDIT && clickToEdit) {
this.clickNum += 1;
_.debounce(() => {
if (this.clickNum === 1) {
clickFn();
}
this.clickNum = 0;
}, Const.DELAY_FOR_DBCLICK)();
}, DELAY_FOR_DBCLICK)();
} else {
clickFn();
}
@@ -72,8 +75,11 @@ class Row extends Component {
const {
mode,
onStart,
EditingCell,
ridx: editingRowIdx,
cidx: editingColIdx,
CLICK_TO_CELL_EDIT,
DBCLICK_TO_CELL_EDIT,
...rest
} = cellEdit;
@@ -136,9 +142,10 @@ class Row extends Component {
rowIndex={ rowIndex }
columnIndex={ index }
column={ column }
editMode={ mode }
editable={ editable }
onStart={ onStart }
editable={ editable }
clickToEdit={ mode === CLICK_TO_CELL_EDIT }
dbclickToEdit={ mode === DBCLICK_TO_CELL_EDIT }
/>
);
})