diff --git a/docs/cell-edit.md b/docs/cell-edit.md index 051dde8..06968c7 100644 --- a/docs/cell-edit.md +++ b/docs/cell-edit.md @@ -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) diff --git a/packages/react-bootstrap-table2-editor/package.json b/packages/react-bootstrap-table2-editor/package.json new file mode 100644 index 0000000..8b7aab8 --- /dev/null +++ b/packages/react-bootstrap-table2-editor/package.json @@ -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" +} diff --git a/packages/react-bootstrap-table2-editor/src/const.js b/packages/react-bootstrap-table2-editor/src/const.js new file mode 100644 index 0000000..acbef9d --- /dev/null +++ b/packages/react-bootstrap-table2-editor/src/const.js @@ -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'; diff --git a/packages/react-bootstrap-table2-editor/src/editing-cell.js b/packages/react-bootstrap-table2-editor/src/editing-cell.js new file mode 100644 index 0000000..5e54984 --- /dev/null +++ b/packages/react-bootstrap-table2-editor/src/editing-cell.js @@ -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 ( + + this.editor = node } + defaultValue={ value } + className={ editorClass } + { ...editorAttrs } + /> + { hasError ? : null } + + ); + } + }; diff --git a/packages/react-bootstrap-table2/src/cell-edit/editor-indicator.js b/packages/react-bootstrap-table2-editor/src/editor-indicator.js similarity index 100% rename from packages/react-bootstrap-table2/src/cell-edit/editor-indicator.js rename to packages/react-bootstrap-table2-editor/src/editor-indicator.js diff --git a/packages/react-bootstrap-table2-editor/src/index.js b/packages/react-bootstrap-table2-editor/src/index.js new file mode 100644 index 0000000..0d8be80 --- /dev/null +++ b/packages/react-bootstrap-table2-editor/src/index.js @@ -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 +}); diff --git a/packages/react-bootstrap-table2/src/cell-edit/text-editor.js b/packages/react-bootstrap-table2-editor/src/text-editor.js similarity index 100% rename from packages/react-bootstrap-table2/src/cell-edit/text-editor.js rename to packages/react-bootstrap-table2-editor/src/text-editor.js diff --git a/packages/react-bootstrap-table2/src/cell-edit/wrapper.js b/packages/react-bootstrap-table2-editor/src/wrapper.js similarity index 59% rename from packages/react-bootstrap-table2/src/cell-edit/wrapper.js rename to packages/react-bootstrap-table2-editor/src/wrapper.js index 7f21e53..97138d4 100644 --- a/packages/react-bootstrap-table2/src/cell-edit/wrapper.js +++ b/packages/react-bootstrap-table2-editor/src/wrapper.js @@ -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); @@ -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(() => ({ @@ -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); @@ -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 ( ); } }; +}; diff --git a/packages/react-bootstrap-table2/test/cell-edit/editing-cell.test.js b/packages/react-bootstrap-table2-editor/test/editing-cell.test.js similarity index 94% rename from packages/react-bootstrap-table2/test/cell-edit/editing-cell.test.js rename to packages/react-bootstrap-table2-editor/test/editing-cell.test.js index 190d759..5eda210 100644 --- a/packages/react-bootstrap-table2/test/cell-edit/editing-cell.test.js +++ b/packages/react-bootstrap-table2-editor/test/editing-cell.test.js @@ -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 => ( + + + { props.children } + +
+); + describe('EditingCell', () => { let wrapper; diff --git a/packages/react-bootstrap-table2/test/cell-edit/text-editor.test.js b/packages/react-bootstrap-table2-editor/test/text-editor.test.js similarity index 94% rename from packages/react-bootstrap-table2/test/cell-edit/text-editor.test.js rename to packages/react-bootstrap-table2-editor/test/text-editor.test.js index e9d9119..845d788 100644 --- a/packages/react-bootstrap-table2/test/cell-edit/text-editor.test.js +++ b/packages/react-bootstrap-table2-editor/test/text-editor.test.js @@ -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; diff --git a/packages/react-bootstrap-table2-editor/test/wrapper.test.js b/packages/react-bootstrap-table2-editor/test/wrapper.test.js new file mode 100644 index 0000000..4e2426e --- /dev/null +++ b/packages/react-bootstrap-table2-editor/test/wrapper.test.js @@ -0,0 +1,330 @@ +import React from 'react'; +import sinon from 'sinon'; +import { shallow } from 'enzyme'; + +import _ from 'react-bootstrap-table2/src/utils'; +import remoteResolver from 'react-bootstrap-table2/src/props-resolver/remote-resolver'; +import Store from 'react-bootstrap-table2/src/store'; +import BootstrapTable from 'react-bootstrap-table2/src/bootstrap-table'; +import cellEditFactory from '../src'; +import * as Const from '../src/const'; +import wrapperFactory from '../src/wrapper'; + +describe('CellEditWrapper', () => { + let wrapper; + let instance; + const onTableChangeCB = sinon.stub(); + const columns = [{ + dataField: 'id', + text: 'ID' + }, { + dataField: 'name', + text: 'Name' + }]; + const data = [{ + id: 1, + name: 'A' + }, { + id: 2, + name: 'B' + }]; + + const createTableProps = (props = {}) => { + const { cellEdit, ...rest } = props; + const tableProps = { + keyField: 'id', + columns, + data, + _, + store: new Store('id'), + cellEdit: cellEditFactory(cellEdit), + onTableChange: onTableChangeCB, + ...rest + }; + tableProps.store.data = data; + return tableProps; + }; + + const CellEditWrapper = wrapperFactory(BootstrapTable, { + _, + remoteResolver + }); + + const createCellEditWrapper = (props, renderFragment = true) => { + wrapper = shallow(); + instance = wrapper.instance(); + if (renderFragment) { + const fragment = instance.render(); + wrapper = shallow(
{ fragment }
); + } + }; + + afterEach(() => { + onTableChangeCB.reset(); + }); + + beforeEach(() => { + const props = createTableProps({ + cellEdit: { mode: Const.CLICK_TO_CELL_EDIT } + }); + createCellEditWrapper(props); + }); + + it('should render CellEditWrapper correctly', () => { + expect(wrapper.length).toBe(1); + expect(wrapper.find(BootstrapTable)).toBeDefined(); + }); + + it('should have correct state', () => { + expect(instance.state.ridx).toBeNull(); + expect(instance.state.cidx).toBeNull(); + expect(instance.state.message).toBeNull(); + expect(instance.state.isDataChanged).toBeFalsy(); + }); + + it('should inject correct props to base component', () => { + const base = wrapper.find(BootstrapTable); + expect(base.props().cellEdit).toBeDefined(); + expect(base.props().cellEdit.onStart).toBeDefined(); + expect(base.props().cellEdit.onEscape).toBeDefined(); + expect(base.props().cellEdit.onUpdate).toBeDefined(); + expect(base.props().cellEdit.EditingCell).toBeDefined(); + expect(base.props().cellEdit.ridx).toBeNull(); + expect(base.props().cellEdit.cidx).toBeNull(); + expect(base.props().cellEdit.message).toBeNull(); + expect(base.props().isDataChanged).toBe(instance.state.isDataChanged); + }); + + describe('when receive new cellEdit prop', () => { + const spy = jest.spyOn(CellEditWrapper.prototype, 'escapeEditing'); + + describe('and cellEdit is not work on remote', () => { + beforeEach(() => { + const props = createTableProps({ + cellEdit: { mode: Const.CLICK_TO_CELL_EDIT } + }); + createCellEditWrapper(props); + wrapper.setProps({ cellEdit: props.cellEdit }); + }); + + it('should always setting state.isDataChanged as false', () => { + expect(instance.state.isDataChanged).toBeFalsy(); + }); + }); + + describe('and cellEdit is work on remote', () => { + let errorMessage; + let props; + beforeEach(() => { + props = createTableProps({ + cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, + remote: true + }); + }); + + describe('and cellEdit.errorMessage is defined', () => { + beforeEach(() => { + createCellEditWrapper(props, false); + errorMessage = 'test'; + const newCellEdit = { + ...props.cellEdit, + options: { ...props.cellEdit.options, errorMessage } + }; + wrapper.setProps({ cellEdit: newCellEdit }); + }); + + it('should setting correct state', () => { + expect(instance.state.isDataChanged).toBeFalsy(); + expect(instance.state.message).toEqual(errorMessage); + }); + }); + + describe('and cellEdit.errorMessage is undefined', () => { + beforeEach(() => { + errorMessage = null; + createCellEditWrapper(props, false); + const newCellEdit = { + ...props.cellEdit, + options: { ...props.cellEdit.options, errorMessage } + }; + wrapper.setProps({ cellEdit: newCellEdit }); + }); + + it('should setting correct state', () => { + expect(wrapper.state().isDataChanged).toBeTruthy(); + }); + + it('should escape current editing', () => { + expect(spy).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('call escapeEditing function', () => { + it('should set state correctly', () => { + instance.escapeEditing(); + expect(instance.state.ridx).toBeNull(); + expect(instance.state.cidx).toBeNull(); + }); + }); + + describe('call startEditing function', () => { + const ridx = 1; + const cidx = 3; + + it('should set state correctly', () => { + instance.startEditing(ridx, cidx); + expect(instance.state.ridx).toEqual(ridx); + expect(instance.state.cidx).toEqual(cidx); + expect(instance.state.isDataChanged).toBeFalsy(); + }); + + describe('if selectRow.clickToSelect is defined', () => { + beforeEach(() => { + const selectRow = { mode: 'checkbox', clickToSelect: true }; + const props = createTableProps({ + cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, + selectRow + }); + createCellEditWrapper(props); + }); + + it('should not set state', () => { + instance.startEditing(ridx, cidx); + expect(instance.state.ridx).toBeNull(); + expect(instance.state.cidx).toBeDefined(); + }); + }); + + describe('if selectRow.clickToSelect and selectRow.clickToEdit is defined', () => { + beforeEach(() => { + const selectRow = { mode: 'checkbox', clickToSelect: true, clickToEdit: true }; + const props = createTableProps({ + cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, + selectRow + }); + createCellEditWrapper(props); + }); + + it('should set state correctly', () => { + instance.startEditing(ridx, cidx); + expect(instance.state.ridx).toEqual(ridx); + expect(instance.state.cidx).toEqual(cidx); + }); + }); + }); + + describe('call completeEditing function', () => { + it('should set state correctly', () => { + instance.completeEditing(); + expect(instance.state.ridx).toBeNull(); + expect(instance.state.cidx).toBeNull(); + expect(instance.state.message).toBeNull(); + expect(instance.state.isDataChanged).toBeTruthy(); + }); + }); + + describe('call handleCellUpdate function', () => { + let props; + const row = data[0]; + const column = columns[1]; + const newValue = 'new name'; + + describe('when cell edit is work on remote', () => { + const spy = jest.spyOn(CellEditWrapper.prototype, 'handleCellChange'); + + beforeEach(() => { + props = createTableProps({ + cellEdit: { mode: Const.CLICK_TO_CELL_EDIT }, + remote: true + }); + createCellEditWrapper(props); + instance.handleCellUpdate(row, column, newValue); + }); + + it('should calling handleCellChange correctly', () => { + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls).toHaveLength(1); + expect(spy.mock.calls[0]).toHaveLength(3); + expect(spy.mock.calls[0][0]).toEqual(row.id); + expect(spy.mock.calls[0][1]).toEqual(column.dataField); + expect(spy.mock.calls[0][2]).toEqual(newValue); + }); + }); + + describe('when cell edit is not work on remote', () => { + const spyOnCompleteEditing = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); + const spyOnStoreEdit = jest.spyOn(Store.prototype, 'edit'); + + beforeEach(() => { + props = createTableProps({ + cellEdit: { mode: Const.CLICK_TO_CELL_EDIT } + }); + createCellEditWrapper(props); + instance.handleCellUpdate(row, column, newValue); + }); + + afterEach(() => { + spyOnStoreEdit.mockReset(); + spyOnCompleteEditing.mockReset(); + }); + + it('should calling props.store.edit', () => { + expect(spyOnStoreEdit).toHaveBeenCalled(); + expect(spyOnStoreEdit.mock.calls).toHaveLength(1); + expect(spyOnStoreEdit.mock.calls[0]).toHaveLength(3); + expect(spyOnStoreEdit.mock.calls[0][0]).toEqual(row.id); + expect(spyOnStoreEdit.mock.calls[0][1]).toEqual(column.dataField); + expect(spyOnStoreEdit.mock.calls[0][2]).toEqual(newValue); + }); + + it('should calling completeEditing function', () => { + expect(spyOnCompleteEditing).toHaveBeenCalled(); + }); + + describe('if cellEdit.afterSaveCell prop defined', () => { + const aftereSaveCellCallBack = sinon.stub(); + + beforeEach(() => { + props = createTableProps({ + cellEdit: { + mode: Const.CLICK_TO_CELL_EDIT, + afterSaveCell: aftereSaveCellCallBack + } + }); + createCellEditWrapper(props); + instance.handleCellUpdate(row, column, newValue); + }); + + it('should calling cellEdit.afterSaveCell correctly', () => { + expect(aftereSaveCellCallBack.callCount).toBe(1); + expect(aftereSaveCellCallBack.calledWith( + row[column.dataField], newValue, row, column) + ).toBe(true); + }); + }); + }); + + describe('if cellEdit.beforeSaveCell prop defined', () => { + const beforeSaveCellCallBack = sinon.stub(); + beforeEach(() => { + props = createTableProps({ + cellEdit: { + mode: Const.CLICK_TO_CELL_EDIT, + beforeSaveCell: beforeSaveCellCallBack + } + }); + createCellEditWrapper(props); + instance.handleCellUpdate(row, column, newValue); + }); + + it('should calling cellEdit.beforeSaveCell correctly', () => { + expect(beforeSaveCellCallBack.callCount).toBe(1); + expect(beforeSaveCellCallBack.calledWith( + row[column.dataField], newValue, row, column) + ).toBe(true); + }); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js index 25f09d8..3cbe327 100644 --- a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js +++ b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js @@ -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'], diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/blur-to-save-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/blur-to-save-table.js index db6818c..ebd875b 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/blur-to-save-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/blur-to-save-table.js @@ -1,6 +1,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -18,6 +19,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -29,26 +32,28 @@ const columns = [{ text: 'Product Price' }]; -const cellEdit = { - mode: 'click', - blurToSave: true -}; - `; -const cellEdit = { - mode: 'click', - blurToSave: true -}; export default () => (
- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-class-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-class-table.js index 3486700..39d6862 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-class-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-class-table.js @@ -2,6 +2,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -22,6 +23,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -36,24 +39,22 @@ const columns = [{ (cell > 2101 ? 'editing-price-bigger-than-2101' : 'editing-price-small-than-2101') }]; -const cellEdit = { - mode: 'click' -}; - `; -const cellEdit = { - mode: 'click' -}; export default () => (
- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-hooks-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-hooks-table.js index 24779fc..f7aa30f 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-hooks-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-hooks-table.js @@ -3,6 +3,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -20,6 +21,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -31,28 +34,30 @@ const columns = [{ text: 'Product Price' }]; -const cellEdit = { - mode: 'click', - beforeSaveCell: (oldValue, newValue, row, column) => { console.log('Before Saving Cell!!'); }, - afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); } -}; - { console.log('Before Saving Cell!!'); }, + afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); } + }) } /> `; -const cellEdit = { - mode: 'click', - beforeSaveCell: (oldValue, newValue, row, column) => { console.log('Before Saving Cell!!'); }, - afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); } -}; export default () => (
- + { console.log('Before Saving Cell!!'); }, + afterSaveCell: (oldValue, newValue, row, column) => { console.log('After Saving Cell!!'); } + }) } + /> { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-style-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-style-table.js index 2101bd3..28ea32f 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-style-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-style-table.js @@ -2,6 +2,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -26,6 +27,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -44,24 +47,22 @@ const columns = [{ } }]; -const cellEdit = { - mode: 'click' -}; - `; -const cellEdit = { - mode: 'click' -}; export default () => (
- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-validator-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-validator-table.js index 6b31c5d..c49823b 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-validator-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-edit-validator-table.js @@ -1,6 +1,7 @@ -import React from 'react'; /* eslint no-unused-vars: 0 */ +import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -33,6 +34,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -59,27 +62,29 @@ const columns = [{ } }]; -const cellEdit = { - mode: 'click', - blurToSave: true -}; - `; -const cellEdit = { - mode: 'click', - blurToSave: true -}; export default () => (

Product Price should bigger than $2000

- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-level-editable-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-level-editable-table.js index 331792a..89e7947 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/cell-level-editable-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/cell-level-editable-table.js @@ -2,6 +2,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -20,6 +21,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -32,24 +35,22 @@ const columns = [{ editable: (content, row, rowIndex, columnIndex) => content > 2101 }]; -const cellEdit = { - mode: 'click' -}; - `; -const cellEdit = { - mode: 'click' -}; export default () => (
- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/click-to-edit-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/click-to-edit-table.js index f48e6e4..5055918 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/click-to-edit-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/click-to-edit-table.js @@ -1,6 +1,8 @@ +/* eslint react/prefer-stateless-function: 0 */ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -18,6 +20,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -29,24 +33,22 @@ const columns = [{ text: 'Product Price' }]; -const cellEdit = { - mode: 'click' -}; - `; -const cellEdit = { - mode: 'click' -}; export default () => (
- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/column-level-editable-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/column-level-editable-table.js index 0242911..f344f9f 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/column-level-editable-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/column-level-editable-table.js @@ -1,6 +1,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -19,6 +20,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -32,26 +35,28 @@ const columns = [{ text: 'Product Price' }]; -const cellEdit = { - mode: 'click', - blurToSave: true -}; - `; -const cellEdit = { - mode: 'click', - blurToSave: true -}; export default () => (
- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/dbclick-to-edit-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/dbclick-to-edit-table.js index c51a155..76eab20 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/dbclick-to-edit-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/dbclick-to-edit-table.js @@ -1,6 +1,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -18,6 +19,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -29,24 +32,22 @@ const columns = [{ text: 'Product Price' }]; -const cellEdit = { - mode: 'dbclick' -}; - `; -const cellEdit = { - mode: 'dbclick' -}; export default () => (
- + { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/cell-edit/row-level-editable-table.js b/packages/react-bootstrap-table2-example/examples/cell-edit/row-level-editable-table.js index 99f177a..c141d8b 100644 --- a/packages/react-bootstrap-table2-example/examples/cell-edit/row-level-editable-table.js +++ b/packages/react-bootstrap-table2-example/examples/cell-edit/row-level-editable-table.js @@ -1,6 +1,7 @@ import React from 'react'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -18,6 +19,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const columns = [{ dataField: 'id', text: 'Product ID' @@ -29,29 +32,29 @@ const columns = [{ text: 'Product Price' }]; -const cellEdit = { - mode: 'click', - blurToSave: true, - // Product ID: 0, 3 will be non-editable - nonEditableRows: () => [0, 3] -}; - [0, 3] + }) } /> `; - -const cellEdit = { - mode: 'click', - blurToSave: true, - nonEditableRows: () => [0, 3] -}; export default () => (
- + [0, 3] + }) } + /> { sourceCode }
); diff --git a/packages/react-bootstrap-table2-example/examples/remote/remote-celledit.js b/packages/react-bootstrap-table2-example/examples/remote/remote-celledit.js index 312c81e..ed8f564 100644 --- a/packages/react-bootstrap-table2-example/examples/remote/remote-celledit.js +++ b/packages/react-bootstrap-table2-example/examples/remote/remote-celledit.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import BootstrapTable from 'react-bootstrap-table2'; +import cellEditFactory from 'react-bootstrap-table2-editor'; import Code from 'components/common/code-block'; import { productsGenerator } from 'utils/common'; @@ -18,6 +19,8 @@ const columns = [{ }]; const sourceCode = `\ +import cellEditFactory from 'react-bootstrap-table2-editor'; +// ... const RemoteCellEdit = (props) => { const cellEdit = { mode: 'click', @@ -31,7 +34,7 @@ const RemoteCellEdit = (props) => { keyField="id" data={ props.data } columns={ columns } - cellEdit={ cellEdit } + cellEdit={ cellEditFactory(cellEdit) } onTableChange={ props.onTableChange } /> { sourceCode } @@ -104,7 +107,7 @@ const RemoteCellEdit = (props) => { keyField="id" data={ props.data } columns={ columns } - cellEdit={ cellEdit } + cellEdit={ cellEditFactory(cellEdit) } onTableChange={ props.onTableChange } /> { sourceCode } diff --git a/packages/react-bootstrap-table2-example/package.json b/packages/react-bootstrap-table2-example/package.json index 419f884..7f85d30 100644 --- a/packages/react-bootstrap-table2-example/package.json +++ b/packages/react-bootstrap-table2-example/package.json @@ -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" diff --git a/packages/react-bootstrap-table2/src/body.js b/packages/react-bootstrap-table2/src/body.js index b0182da..9ee9e89 100644 --- a/packages/react-bootstrap-table2/src/body.js +++ b/packages/react-bootstrap-table2/src/body.js @@ -37,10 +37,10 @@ const Body = (props) => { const indication = _.isFunction(noDataIndication) ? noDataIndication() : noDataIndication; content = ; } 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) diff --git a/packages/react-bootstrap-table2/src/bootstrap-table.js b/packages/react-bootstrap-table2/src/bootstrap-table.js index aeed455..4834ec0 100644 --- a/packages/react-bootstrap-table2/src/bootstrap-table.js +++ b/packages/react-bootstrap-table2/src/bootstrap-table.js @@ -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, diff --git a/packages/react-bootstrap-table2/src/cell-edit/editing-cell.js b/packages/react-bootstrap-table2/src/cell-edit/editing-cell.js deleted file mode 100644 index d353960..0000000 --- a/packages/react-bootstrap-table2/src/cell-edit/editing-cell.js +++ /dev/null @@ -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 ( - - this.editor = node } - defaultValue={ value } - className={ editorClass } - { ...editorAttrs } - /> - { hasError ? : null } - - ); - } -} - -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; diff --git a/packages/react-bootstrap-table2/src/cell.js b/packages/react-bootstrap-table2/src/cell.js index add0f58..d50eeb4 100644 --- a/packages/react-bootstrap-table2/src/cell.js +++ b/packages/react-bootstrap-table2/src/cell.js @@ -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 ( { content } diff --git a/packages/react-bootstrap-table2/src/const.js b/packages/react-bootstrap-table2/src/const.js index e9ebaa6..6c47b1c 100644 --- a/packages/react-bootstrap-table2/src/const.js +++ b/packages/react-bootstrap-table2/src/const.js @@ -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' }; diff --git a/packages/react-bootstrap-table2/src/container.js b/packages/react-bootstrap-table2/src/container.js index 30fe195..195ef25 100644 --- a/packages/react-bootstrap-table2/src/container.js +++ b/packages/react-bootstrap-table2/src/container.js @@ -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) { diff --git a/packages/react-bootstrap-table2/src/props-resolver/index.js b/packages/react-bootstrap-table2/src/props-resolver/index.js index 1aedc64..4871e2d 100644 --- a/packages/react-bootstrap-table2/src/props-resolver/index.js +++ b/packages/react-bootstrap-table2/src/props-resolver/index.js @@ -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 diff --git a/packages/react-bootstrap-table2/src/row.js b/packages/react-bootstrap-table2/src/row.js index b1dea9c..24f443a 100644 --- a/packages/react-bootstrap-table2/src/row.js +++ b/packages/react-bootstrap-table2/src/row.js @@ -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 } /> ); }) diff --git a/packages/react-bootstrap-table2/test/bootstrap-table.test.js b/packages/react-bootstrap-table2/test/bootstrap-table.test.js index 458f456..3e5e0e1 100644 --- a/packages/react-bootstrap-table2/test/bootstrap-table.test.js +++ b/packages/react-bootstrap-table2/test/bootstrap-table.test.js @@ -1,5 +1,4 @@ import React from 'react'; -import sinon from 'sinon'; import { shallow } from 'enzyme'; import Caption from '../src/caption'; @@ -7,7 +6,6 @@ import Store from '../src/store'; import Header from '../src/header'; import Body from '../src/body'; import BootstrapTable from '../src/bootstrap-table'; -import Const from '../src/const'; describe('BootstrapTable', () => { let wrapper; @@ -116,49 +114,4 @@ describe('BootstrapTable', () => { expect(wrapper.find('.table-caption').length).toBe(1); }); }); - - describe('when cellEdit props is defined', () => { - const nonEditableRows = [data[1].id]; - const currEditCell = { - ridx: 1, - cidx: 2, - message: null, - editing: false - }; - const cellEdit = { - mode: Const.CLICK_TO_CELL_EDIT, - onEditing: sinon.stub(), - nonEditableRows: () => nonEditableRows - }; - - beforeEach(() => { - wrapper = shallow( - - ); - }); - - it('should resolve correct cellEdit object to Body component', () => { - const body = wrapper.find(Body); - expect(body.length).toBe(1); - expect(body.props().cellEdit.nonEditableRows).toEqual(nonEditableRows); - expect(body.props().cellEdit.ridx).toEqual(currEditCell.ridx); - expect(body.props().cellEdit.cidx).toEqual(currEditCell.cidx); - expect(body.props().cellEdit.message).toEqual(currEditCell.message); - expect(body.props().cellEdit.editing).toEqual(currEditCell.editing); - expect(body.props().cellEdit.onStart).toBeDefined(); - expect(body.props().cellEdit.onEscape).toBeDefined(); - expect(body.props().cellEdit.onUpdate).toBeDefined(); - }); - }); }); diff --git a/packages/react-bootstrap-table2/test/cell-edit/wrapper.test.js b/packages/react-bootstrap-table2/test/cell-edit/wrapper.test.js deleted file mode 100644 index c195b16..0000000 --- a/packages/react-bootstrap-table2/test/cell-edit/wrapper.test.js +++ /dev/null @@ -1,346 +0,0 @@ -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; - -import Store from '../../src/store'; -import Container from '../../src'; -import BootstrapTable from '../../src/bootstrap-table'; -import wrapperFactory from '../../src/cell-edit/wrapper'; - -describe('CellEditWrapper', () => { - let wrapper; - - const columns = [{ - dataField: 'id', - text: 'ID' - }, { - dataField: 'name', - text: 'Name' - }]; - - const data = [{ - id: 1, - name: 'A' - }, { - id: 2, - name: 'B' - }]; - - const cellEdit = { - mode: 'click' - }; - - const keyField = 'id'; - const store = new Store(keyField); - store.data = data; - - const CellEditWrapper = wrapperFactory(Container); - - beforeEach(() => { - wrapper = shallow( - - ); - }); - - it('should render CellEditWrapper correctly', () => { - expect(wrapper.length).toBe(1); - expect(wrapper.find(BootstrapTable)).toBeDefined(); - }); - - it('should have correct state', () => { - expect(wrapper.state().ridx).toBeNull(); - expect(wrapper.state().cidx).toBeNull(); - expect(wrapper.state().message).toBeNull(); - expect(wrapper.state().isDataChanged).toBeFalsy(); - }); - - it('should inject correct props to base component', () => { - expect(wrapper.props().onCellUpdate).toBeDefined(); - expect(wrapper.props().onStartEditing).toBeDefined(); - expect(wrapper.props().onEscapeEditing).toBeDefined(); - expect(wrapper.props().isDataChanged).toBe(wrapper.state().isDataChanged); - expect(wrapper.props().currEditCell).toBeDefined(); - expect(wrapper.props().currEditCell.ridx).toBeNull(); - expect(wrapper.props().currEditCell.cidx).toBeNull(); - expect(wrapper.props().currEditCell.message).toBeNull(); - }); - - describe('when receive new cellEdit prop', () => { - const spy = jest.spyOn(CellEditWrapper.prototype, 'escapeEditing'); - - describe('and cellEdit is not work on remote', () => { - beforeEach(() => { - wrapper = shallow( - - ); - wrapper.setProps({ cellEdit: { ...cellEdit } }); - }); - - it('should always setting state.isDataChanged as false', () => { - expect(wrapper.state().isDataChanged).toBeFalsy(); - }); - }); - - describe('and cellEdit is work on remote', () => { - let errorMessage; - const ridx = 1; - const cidx = 2; - - describe('and cellEdit.errorMessage is defined', () => { - beforeEach(() => { - wrapper = shallow( - - ); - errorMessage = 'test'; - wrapper.setState({ ridx, cidx }); - wrapper.setProps({ cellEdit: { ...cellEdit, errorMessage } }); - }); - - it('should setting correct state', () => { - expect(wrapper.state().ridx).toEqual(ridx); - expect(wrapper.state().cidx).toEqual(cidx); - expect(wrapper.state().isDataChanged).toBeFalsy(); - expect(wrapper.state().message).toEqual(errorMessage); - }); - }); - - describe('and cellEdit.errorMessage is undefined', () => { - beforeEach(() => { - wrapper = shallow( - - ); - errorMessage = null; - wrapper.setState({ ridx, cidx }); - wrapper.setProps({ cellEdit: { ...cellEdit, errorMessage } }); - }); - - it('should setting correct state', () => { - expect(wrapper.state().isDataChanged).toBeTruthy(); - }); - - it('should escape current editing', () => { - expect(spy).toHaveBeenCalled(); - }); - }); - }); - }); - - describe('call escapeEditing function', () => { - it('should set state correctly', () => { - wrapper.instance().escapeEditing(); - expect(wrapper.state().ridx).toBeNull(); - expect(wrapper.state().cidx).toBeNull(); - }); - }); - - describe('call startEditing function', () => { - const ridx = 1; - const cidx = 3; - it('should set state correctly', () => { - wrapper.instance().startEditing(ridx, cidx); - expect(wrapper.state().ridx).toEqual(ridx); - expect(wrapper.state().cidx).toEqual(cidx); - expect(wrapper.state().isDataChanged).toBeFalsy(); - }); - - describe('if selectRow.clickToSelect is defined', () => { - beforeEach(() => { - const selectRow = { mode: 'checkbox', clickToSelect: true }; - wrapper = shallow( - - ); - }); - - it('should not set state', () => { - wrapper.instance().startEditing(ridx, cidx); - expect(wrapper.state().ridx).toBeNull(); - expect(wrapper.state().cidx).toBeDefined(); - }); - }); - - describe('if selectRow.clickToSelect and selectRow.clickToEdit is defined', () => { - beforeEach(() => { - const selectRow = { mode: 'checkbox', clickToSelect: true, clickToEdit: true }; - wrapper = shallow( - - ); - }); - - it('should set state correctly', () => { - wrapper.instance().startEditing(ridx, cidx); - expect(wrapper.state().ridx).toEqual(ridx); - expect(wrapper.state().cidx).toEqual(cidx); - }); - }); - }); - - describe('call completeEditing function', () => { - it('should set state correctly', () => { - wrapper.instance().completeEditing(); - expect(wrapper.state().ridx).toBeNull(); - expect(wrapper.state().cidx).toBeNull(); - expect(wrapper.state().message).toBeNull(); - expect(wrapper.state().isDataChanged).toBeTruthy(); - }); - }); - - describe('call handleCellUpdate function', () => { - const row = data[0]; - const column = columns[1]; - const newValue = 'new name'; - - describe('when cell edit is work on remote', () => { - const spy = jest.spyOn(CellEditWrapper.prototype, 'handleCellChange'); - const onTableChangeCB = jest.fn(); - - beforeEach(() => { - wrapper = shallow( - - ); - wrapper.instance().handleCellUpdate(row, column, newValue); - }); - - it('should calling handleCellChange correctly', () => { - expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls).toHaveLength(1); - expect(spy.mock.calls[0]).toHaveLength(3); - expect(spy.mock.calls[0][0]).toEqual(row[keyField]); - expect(spy.mock.calls[0][1]).toEqual(column.dataField); - expect(spy.mock.calls[0][2]).toEqual(newValue); - }); - }); - - describe('when cell edit is not work on remote', () => { - const spyOnCompleteEditing = jest.spyOn(CellEditWrapper.prototype, 'completeEditing'); - const spyOnStoreEdit = jest.spyOn(Store.prototype, 'edit'); - - beforeEach(() => { - wrapper = shallow( - - ); - wrapper.instance().handleCellUpdate(row, column, newValue); - }); - - afterEach(() => { - spyOnStoreEdit.mockReset(); - spyOnCompleteEditing.mockReset(); - }); - - it('should calling props.store.edit', () => { - expect(spyOnStoreEdit).toHaveBeenCalled(); - expect(spyOnStoreEdit.mock.calls).toHaveLength(1); - expect(spyOnStoreEdit.mock.calls[0]).toHaveLength(3); - expect(spyOnStoreEdit.mock.calls[0][0]).toEqual(row[keyField]); - expect(spyOnStoreEdit.mock.calls[0][1]).toEqual(column.dataField); - expect(spyOnStoreEdit.mock.calls[0][2]).toEqual(newValue); - }); - - it('should calling completeEditing function', () => { - expect(spyOnCompleteEditing).toHaveBeenCalled(); - }); - - describe('if cellEdit.afterSaveCell prop defined', () => { - const aftereSaveCellCallBack = sinon.stub(); - - beforeEach(() => { - cellEdit.afterSaveCell = aftereSaveCellCallBack; - wrapper = shallow( - - ); - wrapper.instance().handleCellUpdate(row, column, newValue); - }); - - it('should calling cellEdit.afterSaveCell correctly', () => { - expect(aftereSaveCellCallBack.callCount).toBe(1); - expect(aftereSaveCellCallBack.calledWith( - row[column.dataField], newValue, row, column) - ).toBe(true); - }); - }); - }); - - describe('if cellEdit.beforeSaveCell prop defined', () => { - const beforeSaveCellCallBack = sinon.stub(); - beforeEach(() => { - cellEdit.beforeSaveCell = beforeSaveCellCallBack; - wrapper = shallow( - - ); - wrapper.instance().handleCellUpdate(row, column, newValue); - }); - - it('should calling cellEdit.beforeSaveCell correctly', () => { - expect(beforeSaveCellCallBack.callCount).toBe(1); - expect(beforeSaveCellCallBack.calledWith( - row[column.dataField], newValue, row, column) - ).toBe(true); - }); - }); - }); -}); diff --git a/packages/react-bootstrap-table2/test/cell.test.js b/packages/react-bootstrap-table2/test/cell.test.js index 49d9458..b83bcc9 100644 --- a/packages/react-bootstrap-table2/test/cell.test.js +++ b/packages/react-bootstrap-table2/test/cell.test.js @@ -2,7 +2,6 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import Const from '../src/const'; import Cell from '../src/cell'; describe('Cell', () => { @@ -462,7 +461,7 @@ describe('Cell', () => { onStartCallBack = sinon.stub().withArgs(rowIndex, columnIndex); }); - describe(`and editMode is ${Const.CLICK_TO_CELL_EDIT}`, () => { + describe('and clickToEdit is true', () => { beforeEach(() => { wrapper = shallow( { column={ column } columnIndex={ columnIndex } editable - editMode={ Const.CLICK_TO_CELL_EDIT } + clickToEdit onStart={ onStartCallBack } /> ); @@ -502,7 +501,7 @@ describe('Cell', () => { }); }); - describe(`and editMode is ${Const.DBCLICK_TO_CELL_EDIT}`, () => { + describe('and dbclickToEdit is true', () => { beforeEach(() => { wrapper = shallow( { column={ column } columnIndex={ 1 } editable - editMode={ Const.DBCLICK_TO_CELL_EDIT } + dbclickToEdit onStart={ onStartCallBack } /> ); diff --git a/packages/react-bootstrap-table2/test/container.test.js b/packages/react-bootstrap-table2/test/container.test.js index 066a948..291e4cc 100644 --- a/packages/react-bootstrap-table2/test/container.test.js +++ b/packages/react-bootstrap-table2/test/container.test.js @@ -51,8 +51,15 @@ describe('container', () => { }); describe('when cellEdit prop is defined', () => { + const wrapperFactory = Base => class CellEditWrapper extends React.Component { + render() { return ; } + }; + const cellEdit = { - mode: 'click' + wrapperFactory, + options: { + mode: 'click' + } }; beforeEach(() => { diff --git a/packages/react-bootstrap-table2/test/props-resolver/index.test.js b/packages/react-bootstrap-table2/test/props-resolver/index.test.js index 7f4cf59..1b6bf8c 100644 --- a/packages/react-bootstrap-table2/test/props-resolver/index.test.js +++ b/packages/react-bootstrap-table2/test/props-resolver/index.test.js @@ -72,64 +72,6 @@ describe('TableResolver', () => { }); }); - describe('resolveCellEditProps', () => { - describe('if cellEdit prop not defined', () => { - beforeEach(() => { - const mockElement = React.createElement(BootstrapTableMock, { - data, keyField, columns - }, null); - wrapper = shallow(mockElement); - }); - - it('should resolve a default cellEdit instance', () => { - const cellEdit = wrapper.instance().resolveCellEditProps(); - expect(cellEdit).toBeDefined(); - expect(cellEdit.mode).toEqual(Const.UNABLE_TO_CELL_EDIT); - expect(cellEdit.nonEditableRows.length).toEqual(0); - }); - }); - - describe('if cellEdit prop defined', () => { - const expectNonEditableRows = [1, 2]; - const cellEdit = { - mode: Const.DBCLICK_TO_CELL_EDIT, - blurToSave: true, - beforeSaveCell: sinon.stub(), - afterSaveCell: sinon.stub(), - nonEditableRows: sinon.stub().returns(expectNonEditableRows) - }; - - beforeEach(() => { - const mockElement = React.createElement(BootstrapTableMock, { - data, keyField, columns, cellEdit - }, null); - wrapper = shallow(mockElement); - }); - - it('should resolve a cellEdit correctly', () => { - const cellEditInfo = wrapper.instance().resolveCellEditProps(); - expect(cellEditInfo).toBeDefined(); - expect(cellEditInfo.mode).toEqual(cellEdit.mode); - expect(cellEditInfo.onUpdate).toEqual(cellEdit.onUpdate); - expect(cellEditInfo.blurToSave).toEqual(cellEdit.blurToSave); - expect(cellEditInfo.beforeSaveCell).toEqual(cellEdit.beforeSaveCell); - expect(cellEditInfo.afterSaveCell).toEqual(cellEdit.afterSaveCell); - expect(cellEditInfo.nonEditableRows).toEqual(expectNonEditableRows); - }); - - it('should attach options to cellEdit props', () => { - const something = { - test: 1, - cb: sinon.stub() - }; - const cellEditInfo = wrapper.instance().resolveCellEditProps(something); - expect(cellEditInfo).toBeDefined(); - expect(cellEditInfo.test).toEqual(something.test); - expect(cellEditInfo.cb).toEqual(something.cb); - }); - }); - }); - describe('resolveSelectRowProps', () => { let cellSelectionInfo; let selectRow; diff --git a/packages/react-bootstrap-table2/test/row.test.js b/packages/react-bootstrap-table2/test/row.test.js index a44e52f..955a421 100644 --- a/packages/react-bootstrap-table2/test/row.test.js +++ b/packages/react-bootstrap-table2/test/row.test.js @@ -5,7 +5,6 @@ import { shallow } from 'enzyme'; import Cell from '../src/cell'; import Row from '../src/row'; import Const from '../src/const'; -import EditingCell from '../src/cell-edit/editing-cell'; import SelectionCell from '../src//row-selection/selection-cell'; import mockBodyResolvedProps from './test-helpers/mock/body-resolved-props'; @@ -97,7 +96,9 @@ describe('Row', () => { beforeEach(() => { columns = defaultColumns; cellEdit = { - mode: Const.CLICK_TO_CELL_EDIT + mode: 'click', + CLICK_TO_CELL_EDIT: 'click', + DBCLICK_TO_CELL_EDIT: 'dbclick' }; wrapper = shallow( { } }); - it('Cell component should receive correct editMode props', () => { + it('Cell component should receive correct clickToEdit props', () => { expect(wrapper.length).toBe(1); for (let i = 0; i < columns.length; i += 1) { - expect(wrapper.find(Cell).get(i).props.editMode).toEqual(cellEdit.mode); + expect(wrapper.find(Cell).get(i).props.clickToEdit).toBeTruthy(); } }); + it('Cell component should receive correct dbclickToEdit props', () => { + expect(wrapper.length).toBe(1); + for (let i = 0; i < columns.length; i += 1) { + expect(wrapper.find(Cell).get(i).props.dbclickToEdit).toBeFalsy(); + } + }); + + describe('when props.cellEdit.mode is dbclick', () => { + beforeEach(() => { + cellEdit.mode = cellEdit.DBCLICK_TO_CELL_EDIT; + wrapper = shallow( + + ); + }); + + it('Cell component should receive correct clickToEdit props', () => { + expect(wrapper.length).toBe(1); + for (let i = 0; i < columns.length; i += 1) { + expect(wrapper.find(Cell).get(i).props.clickToEdit).toBeFalsy(); + } + }); + + it('Cell component should receive correct dbclickToEdit props', () => { + expect(wrapper.length).toBe(1); + for (let i = 0; i < columns.length; i += 1) { + expect(wrapper.find(Cell).get(i).props.dbclickToEdit).toBeTruthy(); + } + }); + }); + describe('and column.editable defined false', () => { const nonEditableColIndex = 1; beforeEach(() => { @@ -266,6 +304,7 @@ describe('Row', () => { // Means a cell now is undering editing describe('when cellEdit.ridx and cellEdit.cidx is defined', () => { + const EditingCell = () => null; describe('and cellEdit.ridx is match to current row index', () => { const editingColIndex = 1; beforeEach(() => { @@ -273,6 +312,7 @@ describe('Row', () => { cellEdit.cidx = editingColIndex; cellEdit.onUpdate = sinon.stub(); cellEdit.onEscape = sinon.stub(); + cellEdit.EditingCell = EditingCell; wrapper = shallow( ( - - - { props.children } - -
-); -