|
|
@@ -3,20 +3,28 @@ import PropTypes from 'prop-types';
|
|
|
|
|
|
import Modal from 'react-bootstrap/es/Modal';
|
|
|
import Button from 'react-bootstrap/es/Button';
|
|
|
-import Navbar from 'react-bootstrap/es/Navbar';
|
|
|
import ButtonGroup from 'react-bootstrap/es/ButtonGroup';
|
|
|
|
|
|
import { debounce } from 'throttle-debounce';
|
|
|
+import Collapse from 'react-bootstrap/es/Collapse';
|
|
|
+import FormGroup from 'react-bootstrap/es/FormGroup';
|
|
|
+import ControlLabel from 'react-bootstrap/es/ControlLabel';
|
|
|
+import FormControl from 'react-bootstrap/es/FormControl';
|
|
|
|
|
|
import Handsontable from 'handsontable';
|
|
|
import { HotTable } from '@handsontable/react';
|
|
|
|
|
|
import MarkdownTable from '../../models/MarkdownTable';
|
|
|
-import HandsontableUtil from './HandsontableUtil';
|
|
|
|
|
|
const DEFAULT_HOT_HEIGHT = 300;
|
|
|
+const MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING = {
|
|
|
+ 'r': 'htRight',
|
|
|
+ 'c': 'htCenter',
|
|
|
+ 'l': 'htLeft',
|
|
|
+ '': ''
|
|
|
+};
|
|
|
|
|
|
-export default class HandsontableModal extends React.Component {
|
|
|
+export default class HandsontableModal extends React.PureComponent {
|
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
@@ -24,22 +32,39 @@ export default class HandsontableModal extends React.Component {
|
|
|
|
|
|
this.state = {
|
|
|
show: false,
|
|
|
+ isDataImportAreaExpanded: false,
|
|
|
isWindowExpanded: false,
|
|
|
markdownTableOnInit: HandsontableModal.getDefaultMarkdownTable(),
|
|
|
markdownTable: HandsontableModal.getDefaultMarkdownTable(),
|
|
|
handsontableHeight: DEFAULT_HOT_HEIGHT,
|
|
|
- handsontableSetting: HandsontableModal.getDefaultHandsontableSetting()
|
|
|
};
|
|
|
|
|
|
this.init = this.init.bind(this);
|
|
|
this.reset = this.reset.bind(this);
|
|
|
this.cancel = this.cancel.bind(this);
|
|
|
this.save = this.save.bind(this);
|
|
|
+ this.afterLoadDataHandler = this.afterLoadDataHandler.bind(this);
|
|
|
+ this.beforeColumnMoveHandler = this.beforeColumnMoveHandler.bind(this);
|
|
|
+ this.beforeColumnResizeHandler = this.beforeColumnResizeHandler.bind(this);
|
|
|
+ this.afterColumnResizeHandler = this.afterColumnResizeHandler.bind(this);
|
|
|
+ this.modifyColWidthHandler = this.modifyColWidthHandler.bind(this);
|
|
|
+ this.synchronizeAlignment = this.synchronizeAlignment.bind(this);
|
|
|
+ this.alignButtonHandler = this.alignButtonHandler.bind(this);
|
|
|
+ this.toggleDataImportArea = this.toggleDataImportArea.bind(this);
|
|
|
this.expandWindow = this.expandWindow.bind(this);
|
|
|
this.contractWindow = this.contractWindow.bind(this);
|
|
|
|
|
|
// create debounced method for expanding HotTable
|
|
|
this.expandHotTableHeightWithDebounce = debounce(100, this.expandHotTableHeight);
|
|
|
+
|
|
|
+ // a Set instance that stores column indices which are resized manually.
|
|
|
+ // these columns will NOT be determined the width automatically by 'modifyColWidthHandler'
|
|
|
+ this.manuallyResizedColumnIndicesSet = new Set();
|
|
|
+
|
|
|
+ // generate setting object for HotTable instance
|
|
|
+ this.handsontableSettings = Object.assign(HandsontableModal.getDefaultHandsontableSetting(), {
|
|
|
+ contextMenu: this.createCustomizedContextMenu(),
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
init(markdownTable) {
|
|
|
@@ -48,19 +73,42 @@ export default class HandsontableModal extends React.Component {
|
|
|
{
|
|
|
markdownTableOnInit: initMarkdownTable,
|
|
|
markdownTable: initMarkdownTable.clone(),
|
|
|
- handsontableSetting: Object.assign({}, this.state.handsontableSetting, {
|
|
|
- /*
|
|
|
- * The afterUpdateSettings hook is called when this component state changes.
|
|
|
- *
|
|
|
- * In detail, when this component state changes, React will re-render HotTable because it is passed some state values of this component.
|
|
|
- * HotTable#shouldComponentUpdate is called in this process and it call the updateSettings method for the Handsontable instance.
|
|
|
- * After updateSetting is executed, Handsontable calls a AfterUpdateSetting hook.
|
|
|
- */
|
|
|
- //// commented out and will be fixed by GC-1203 -- 2018.10.19 Yuki Takei
|
|
|
- // afterUpdateSettings: HandsontableUtil.createHandlerToSynchronizeHandontableAlignWith(initMarkdownTable.options.align)
|
|
|
- })
|
|
|
}
|
|
|
);
|
|
|
+
|
|
|
+ this.manuallyResizedColumnIndicesSet.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ createCustomizedContextMenu() {
|
|
|
+ return {
|
|
|
+ items: {
|
|
|
+ 'row_above': {}, 'row_below': {}, 'col_left': {}, 'col_right': {},
|
|
|
+ 'separator1': Handsontable.plugins.ContextMenu.SEPARATOR,
|
|
|
+ 'remove_row': {}, 'remove_col': {},
|
|
|
+ 'separator2': Handsontable.plugins.ContextMenu.SEPARATOR,
|
|
|
+ 'custom_alignment': {
|
|
|
+ name: 'Align columns',
|
|
|
+ key: 'align_columns',
|
|
|
+ submenu: {
|
|
|
+ items: [
|
|
|
+ {
|
|
|
+ name: 'Left',
|
|
|
+ key: 'align_columns:1',
|
|
|
+ callback: (key, selection) => {this.align('l', selection[0].start.col, selection[0].end.col)}
|
|
|
+ }, {
|
|
|
+ name: 'Center',
|
|
|
+ key: 'align_columns:2',
|
|
|
+ callback: (key, selection) => {this.align('c', selection[0].start.col, selection[0].end.col)}
|
|
|
+ }, {
|
|
|
+ name: 'Right',
|
|
|
+ key: 'align_columns:3',
|
|
|
+ callback: (key, selection) => {this.align('r', selection[0].start.col, selection[0].end.col)}
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
show(markdownTable) {
|
|
|
@@ -68,27 +116,113 @@ export default class HandsontableModal extends React.Component {
|
|
|
this.setState({ show: true });
|
|
|
}
|
|
|
|
|
|
+ hide() {
|
|
|
+ this.setState({
|
|
|
+ show: false,
|
|
|
+ isDataImportAreaExpanded: false,
|
|
|
+ isWindowExpanded: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
reset() {
|
|
|
this.setState({ markdownTable: this.state.markdownTableOnInit.clone() });
|
|
|
}
|
|
|
|
|
|
cancel() {
|
|
|
- this.setState({ show: false });
|
|
|
+ this.hide();
|
|
|
}
|
|
|
|
|
|
save() {
|
|
|
- let newMarkdownTable = this.state.markdownTable.clone();
|
|
|
- //// commented out and will be fixed by GC-1203 -- 2018.10.19 Yuki Takei
|
|
|
- // newMarkdownTable.options.align = HandsontableUtil.getMarkdownTableAlignmentFrom(this.refs.hotTable.hotInstance);
|
|
|
-
|
|
|
if (this.props.onSave != null) {
|
|
|
- this.props.onSave(newMarkdownTable);
|
|
|
+ this.props.onSave(this.state.markdownTable);
|
|
|
}
|
|
|
|
|
|
- this.setState({ show: false });
|
|
|
+ this.hide();
|
|
|
}
|
|
|
|
|
|
- setClassNameToColumns(className) {
|
|
|
+ afterLoadDataHandler(initialLoad) {
|
|
|
+ // clear 'manuallyResizedColumnIndicesSet' for the first loading
|
|
|
+ if (initialLoad) {
|
|
|
+ this.manuallyResizedColumnIndicesSet.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ this.synchronizeAlignment();
|
|
|
+ }
|
|
|
+
|
|
|
+ beforeColumnMoveHandler(columns, target) {
|
|
|
+ // clear 'manuallyResizedColumnIndicesSet'
|
|
|
+ this.manuallyResizedColumnIndicesSet.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ beforeColumnResizeHandler(currentColumn) {
|
|
|
+ /*
|
|
|
+ * The following bug disturbs to use 'beforeColumnResizeHandler' to store column index -- 2018.10.23 Yuki Takei
|
|
|
+ * https://github.com/handsontable/handsontable/issues/3328
|
|
|
+ *
|
|
|
+ * At the moment, using 'afterColumnResizeHandler' instead.
|
|
|
+ */
|
|
|
+
|
|
|
+ // store column index
|
|
|
+ // this.manuallyResizedColumnIndicesSet.add(currentColumn);
|
|
|
+ }
|
|
|
+
|
|
|
+ afterColumnResizeHandler(currentColumn) {
|
|
|
+ /*
|
|
|
+ * The following bug disturbs to use 'beforeColumnResizeHandler' to store column index -- 2018.10.23 Yuki Takei
|
|
|
+ * https://github.com/handsontable/handsontable/issues/3328
|
|
|
+ *
|
|
|
+ * At the moment, using 'afterColumnResizeHandler' instead.
|
|
|
+ */
|
|
|
+
|
|
|
+ // store column index
|
|
|
+ this.manuallyResizedColumnIndicesSet.add(currentColumn);
|
|
|
+ }
|
|
|
+
|
|
|
+ modifyColWidthHandler(width, column) {
|
|
|
+ // return original width if the column index exists in 'manuallyResizedColumnIndicesSet'
|
|
|
+ if (this.manuallyResizedColumnIndicesSet.has(column)) {
|
|
|
+ return width;
|
|
|
+ }
|
|
|
+ // return fixed width if first initializing
|
|
|
+ return Math.max(80, Math.min(400, width));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * change the markdownTable alignment and synchronize the handsontable alignment to it
|
|
|
+ */
|
|
|
+ align(direction, startCol, endCol) {
|
|
|
+ this.setState((prevState) => {
|
|
|
+ // change only align info, so share table data to avoid redundant copy
|
|
|
+ const newMarkdownTable = new MarkdownTable(prevState.markdownTable.table, {align: [].concat(prevState.markdownTable.options.align)});
|
|
|
+ for (let i = startCol; i <= endCol ; i++) {
|
|
|
+ newMarkdownTable.options.align[i] = direction;
|
|
|
+ }
|
|
|
+ return { markdownTable: newMarkdownTable };
|
|
|
+ }, () => {
|
|
|
+ this.synchronizeAlignment();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * synchronize the handsontable alignment to the markdowntable alignment
|
|
|
+ */
|
|
|
+ synchronizeAlignment() {
|
|
|
+ if (this.refs.hotTable == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const align = this.state.markdownTable.options.align;
|
|
|
+ const hotInstance = this.refs.hotTable.hotInstance;
|
|
|
+
|
|
|
+ for (let i = 0; i < align.length; i++) {
|
|
|
+ for (let j = 0; j < hotInstance.countRows(); j++) {
|
|
|
+ hotInstance.setCellMeta(j, i, 'className', MARKDOWNTABLE_TO_HANDSONTABLE_ALIGNMENT_SYMBOL_MAPPING[align[i]]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ hotInstance.render();
|
|
|
+ }
|
|
|
+
|
|
|
+ alignButtonHandler(direction) {
|
|
|
const selectedRange = this.refs.hotTable.hotInstance.getSelectedRange();
|
|
|
if (selectedRange == null) return;
|
|
|
|
|
|
@@ -104,14 +238,18 @@ export default class HandsontableModal extends React.Component {
|
|
|
endCol = selectedRange[0].from.col;
|
|
|
}
|
|
|
|
|
|
- HandsontableUtil.setClassNameToColumns(this.refs.hotTable.hotInstance, startCol, endCol, className);
|
|
|
+ this.align(direction, startCol, endCol);
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleDataImportArea() {
|
|
|
+ this.setState({ isDataImportAreaExpanded: !this.state.isDataImportAreaExpanded });
|
|
|
}
|
|
|
|
|
|
expandWindow() {
|
|
|
this.setState({ isWindowExpanded: true });
|
|
|
|
|
|
// invoke updateHotTableHeight method with delay
|
|
|
- // cz. Resizing this.refs.hotTableContainer is completeted after a little delay after 'isWindowExpanded' set with 'true'
|
|
|
+ // cz. Resizing this.refs.hotTableContainer is completed after a little delay after 'isWindowExpanded' set with 'true'
|
|
|
this.expandHotTableHeightWithDebounce();
|
|
|
}
|
|
|
|
|
|
@@ -155,19 +293,47 @@ export default class HandsontableModal extends React.Component {
|
|
|
<Modal.Title>Edit Table</Modal.Title>
|
|
|
</Modal.Header>
|
|
|
<Modal.Body className="p-0 d-flex flex-column">
|
|
|
- <Navbar className="mb-0">
|
|
|
- <Navbar.Form>
|
|
|
- {/* commented out and will be fixed by GC-1203 -- 2018.10.19 Yuki Takei
|
|
|
- <ButtonGroup>
|
|
|
- <Button onClick={() => { this.setClassNameToColumns('htLeft') }}><i className="ti-align-left"></i></Button>
|
|
|
- <Button onClick={() => { this.setClassNameToColumns('htCenter') }}><i className="ti-align-center"></i></Button>
|
|
|
- <Button onClick={() => { this.setClassNameToColumns('htRight') }}><i className="ti-align-right"></i></Button>
|
|
|
- </ButtonGroup>
|
|
|
- */}
|
|
|
- </Navbar.Form>
|
|
|
- </Navbar>
|
|
|
+ <div className="px-4 py-3 modal-navbar">
|
|
|
+ <Button className="m-r-20 data-import-button" onClick={this.toggleDataImportArea}>
|
|
|
+ Data Import<i className={this.state.isDataImportAreaExpanded ? 'fa fa-angle-up' : 'fa fa-angle-down' }></i>
|
|
|
+ </Button>
|
|
|
+ <ButtonGroup>
|
|
|
+ <Button onClick={() => { this.alignButtonHandler('l') }}><i className="ti-align-left"></i></Button>
|
|
|
+ <Button onClick={() => { this.alignButtonHandler('c') }}><i className="ti-align-center"></i></Button>
|
|
|
+ <Button onClick={() => { this.alignButtonHandler('r') }}><i className="ti-align-right"></i></Button>
|
|
|
+ </ButtonGroup>
|
|
|
+ <Collapse in={this.state.isDataImportAreaExpanded}>
|
|
|
+ <div> {/* This div is necessary for smoothing animations. (https://react-bootstrap.github.io/utilities/transitions/#transitions-collapse) */}
|
|
|
+ <form action="" className="data-import-form pt-5">
|
|
|
+ <FormGroup>
|
|
|
+ <ControlLabel>Select Data Format</ControlLabel>
|
|
|
+ <FormControl componentClass="select" placeholder="select">
|
|
|
+ <option value="select">CSV</option>
|
|
|
+ <option value="other">TSV</option>
|
|
|
+ <option value="other">HTML</option>
|
|
|
+ </FormControl>
|
|
|
+ </FormGroup>
|
|
|
+ <FormGroup>
|
|
|
+ <ControlLabel>Import Data</ControlLabel>
|
|
|
+ <FormControl componentClass="textarea" placeholder="Paste table data" style={{ height: 200 }} />
|
|
|
+ </FormGroup>
|
|
|
+ <div className="d-flex justify-content-end">
|
|
|
+ <Button bsStyle="default" onClick={this.toggleDataImportArea}>Cancel</Button>
|
|
|
+ <Button bsStyle="primary">Import</Button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </Collapse>
|
|
|
+ </div>
|
|
|
<div ref="hotTableContainer" className="m-4 hot-table-container">
|
|
|
- <HotTable ref='hotTable' data={this.state.markdownTable.table} settings={this.state.handsontableSetting} height={this.state.handsontableHeight} />
|
|
|
+ <HotTable ref='hotTable' data={this.state.markdownTable.table}
|
|
|
+ settings={this.handsontableSettings} height={this.state.handsontableHeight}
|
|
|
+ afterLoadData={this.afterLoadDataHandler}
|
|
|
+ modifyColWidth={this.modifyColWidthHandler}
|
|
|
+ beforeColumnMove={this.beforeColumnMoveHandler}
|
|
|
+ beforeColumnResize={this.beforeColumnResizeHandler}
|
|
|
+ afterColumnResize={this.afterColumnResizeHandler}
|
|
|
+ />
|
|
|
</div>
|
|
|
</Modal.Body>
|
|
|
<Modal.Footer>
|
|
|
@@ -206,44 +372,6 @@ export default class HandsontableModal extends React.Component {
|
|
|
manualColumnResize: true,
|
|
|
selectionMode: 'multiple',
|
|
|
outsideClickDeselects: false,
|
|
|
-
|
|
|
- modifyColWidth: function(width) {
|
|
|
- return Math.max(80, Math.min(400, width));
|
|
|
- },
|
|
|
-
|
|
|
- contextMenu: {
|
|
|
- items: {
|
|
|
- 'row_above': {}, 'row_below': {}, 'col_left': {}, 'col_right': {},
|
|
|
- 'separator1': Handsontable.plugins.ContextMenu.SEPARATOR,
|
|
|
- 'remove_row': {}, 'remove_col': {},
|
|
|
- 'separator2': Handsontable.plugins.ContextMenu.SEPARATOR,
|
|
|
- //// commented out and will be fixed by GC-1203 -- 2018.10.19 Yuki Takei
|
|
|
- // 'custom_alignment': {
|
|
|
- // name: 'Align columns',
|
|
|
- // key: 'align_columns',
|
|
|
- // submenu: {
|
|
|
- // items: [{
|
|
|
- // name: 'Left',
|
|
|
- // key: 'align_columns:1',
|
|
|
- // callback: function(key, selection) {
|
|
|
- // HandsontableUtil.setClassNameToColumns(this, selection[0].start.col, selection[0].end.col, 'htLeft');
|
|
|
- // }}, {
|
|
|
- // name: 'Center',
|
|
|
- // key: 'align_columns:2',
|
|
|
- // callback: function(key, selection) {
|
|
|
- // HandsontableUtil.setClassNameToColumns(this, selection[0].start.col, selection[0].end.col, 'htCenter');
|
|
|
- // }}, {
|
|
|
- // name: 'Right',
|
|
|
- // key: 'align_columns:3',
|
|
|
- // callback: function(key, selection) {
|
|
|
- // HandsontableUtil.setClassNameToColumns(this, selection[0].start.col, selection[0].end.col, 'htRight');
|
|
|
- // }}
|
|
|
- // ]
|
|
|
- // }
|
|
|
- // }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
};
|
|
|
}
|
|
|
}
|