Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/cell-edit.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Cell Editing
Before start to use cell edit, please remember to install `react-bootstrap-table2-editor`

```sh
$ npm install react-bootstrap-table2-editor --save
```

# Properties on cellEdit prop
* [mode (**required**)](#mode)
* [blurToSave](#blurToSave)
Expand Down
11 changes: 11 additions & 0 deletions packages/react-bootstrap-table2-editor/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
4 changes: 4 additions & 0 deletions packages/react-bootstrap-table2-editor/src/const.js
Original file line number Diff line number Diff line change
@@ -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';
153 changes: 153 additions & 0 deletions packages/react-bootstrap-table2-editor/src/editing-cell.js
Original file line number Diff line number Diff line change
@@ -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>
);
}
};
16 changes: 16 additions & 0 deletions packages/react-bootstrap-table2-editor/src/index.js
Original file line number Diff line number Diff line change
@@ -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
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
/* 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';

import { CLICK_TO_CELL_EDIT, DBCLICK_TO_CELL_EDIT } from './const';

export default (
Base,
{ _, remoteResolver }
) => {
let EditingCell;
return 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
})
}

export default Base =>
class CellEditWrapper extends remoteResolver(Component) {
constructor(props) {
super(props);
EditingCell = props.cellEdit.editingCellFactory(_);
this.startEditing = this.startEditing.bind(this);
this.escapeEditing = this.escapeEditing.bind(this);
this.completeEditing = this.completeEditing.bind(this);
Expand All @@ -21,10 +40,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(() => ({
Expand All @@ -41,7 +60,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);
Expand Down Expand Up @@ -84,17 +103,33 @@ export default Base =>
}

render() {
const { isDataChanged, ...rest } = this.state;
const { isDataChanged, ...stateRest } = this.state;
const {
cellEdit: {
options: { nonEditableRows, errorMessage, ...optionsRest },
editingCellFactory,
...cellEditRest
}
} = this.props;
const newCellEdit = {
...optionsRest,
...cellEditRest,
...stateRest,
EditingCell,
nonEditableRows: _.isDefined(nonEditableRows) ? nonEditableRows() : [],
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 }
/>
);
}
};
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
/* eslint react/prop-types: 0 */
import 'jsdom-global/register';
import React from 'react';
import sinon from 'sinon';
import { shallow, mount } from 'enzyme';

import { TableRowWrapper } from '../test-helpers/table-wrapper';
import EditingCell from '../../src/cell-edit/editing-cell';
import TextEditor from '../../src/cell-edit/text-editor';
import EditorIndicator from '../../src/cell-edit/editor-indicator';
import _ from 'react-bootstrap-table2/src/utils';
import editingCellFactory from '../src/editing-cell';
import TextEditor from '../src/text-editor';
import EditorIndicator from '../src/editor-indicator';

const EditingCell = editingCellFactory(_);
const TableRowWrapper = props => (
<table>
<tbody>
<tr>{ props.children }</tr>
</tbody>
</table>
);


describe('EditingCell', () => {
let wrapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'jsdom-global/register';
import React from 'react';
import { mount } from 'enzyme';

import TextEditor from '../../src/cell-edit/text-editor';
import TextEditor from '../src/text-editor';

describe('TextEditor', () => {
let wrapper;
Expand Down
Loading