diff --git a/ng/CMakeLists.txt b/ng/CMakeLists.txt index 537b5b93e5..fb0c01c965 100644 --- a/ng/CMakeLists.txt +++ b/ng/CMakeLists.txt @@ -303,6 +303,7 @@ set (NG_JS_SRC_FILES ${NG_SRC_DIR}/src/web/components/sortable/emptyrow.js ${NG_SRC_DIR}/src/web/components/sortable/grid.js ${NG_SRC_DIR}/src/web/components/sortable/item.js + ${NG_SRC_DIR}/src/web/components/sortable/resizer.js ${NG_SRC_DIR}/src/web/components/sortable/row.js ${NG_SRC_DIR}/src/web/components/sticky/container.js ${NG_SRC_DIR}/src/web/components/sticky/sticky.js diff --git a/ng/src/web/components/sortable/grid.js b/ng/src/web/components/sortable/grid.js index b4474d1fd7..97b0749712 100644 --- a/ng/src/web/components/sortable/grid.js +++ b/ng/src/web/components/sortable/grid.js @@ -51,6 +51,13 @@ export const createItem = callback => { }; }; +const updateRow = (row, data) => { + return { + ...row, + ...data, + }; +}; + class Grid extends React.Component { static propTypes = { @@ -68,6 +75,25 @@ class Grid extends React.Component { this.handleDragEnd = this.handleDragEnd.bind(this); this.handleDragStart = this.handleDragStart.bind(this); + this.handleRowResize = this.handleRowResize.bind(this); + } + + notifyChange(items) { + const {onChange} = this.props; + + if (is_defined(onChange)) { + onChange(items); + } + } + + handleRowResize(row, height) { + let {items} = this.props; + items = [...items]; + + const rowIndex = findRowIndex(items, row.id); + items[rowIndex] = updateRow(row, {height}); + + this.notifyChange(items); } handleDragStart(drag) { @@ -110,10 +136,8 @@ class Grid extends React.Component { if (destrowId === 'empty') { // update row - items[sourcerowIndex] = { - id: sourcerowId, - items: sourceRowItems, - }; + items[sourcerowIndex] = updateRow(sourceRow, + {id: sourcerowId, items: sourceRowItems}); // create new row with the removed item items = [...items, createRow([item])]; @@ -122,25 +146,19 @@ class Grid extends React.Component { // add at position destindex sourceRowItems.splice(destIndex, 0, item); - items[sourcerowIndex] = { - id: sourcerowId, - items: sourceRowItems, - }; + items[sourcerowIndex] = updateRow(sourceRow, + {id: sourcerowId, items: sourceRowItems}); } else { - items[sourcerowIndex] = { - id: sourcerowId, - items: sourceRowItems, - }; + items[sourcerowIndex] = updateRow(sourceRow, + {id: sourcerowId, items: sourceRowItems}); // add to destination row const destrowItems = [...destRow.items]; destrowItems.splice(destIndex, 0, item); - items[destrowIndex] = { - id: destrowId, - items: destrowItems, - }; + items[destrowIndex] = updateRow(destRow, + {id: destrowId, items: destrowItems}); } // remove possible empty last row @@ -149,11 +167,7 @@ class Grid extends React.Component { items.pop(); } - const {onChange} = this.props; - - if (is_defined(onChange)) { - onChange(items); - } + this.notifyChange(items); } render() { @@ -174,6 +188,8 @@ class Grid extends React.Component { key={row.id} id={row.id} dropDisabled={disabled} + height={row.height} + onResize={height => this.handleRowResize(row, height)} > {row.items.map((item, index) => ( + * + * Copyright: + * Copyright (C) 2018 Greenbone Networks GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import React from 'react'; + +import glamorous from 'glamorous'; + +import {is_defined, throttleAnimation} from 'gmp/utils'; + +import PropTypes from '../../utils/proptypes'; + +const ResizeContainer = glamorous.div({ + cursor: 'row-resize', + height: '10px', + width: '100%', + zIndex: '1', + display: 'flex', + flexGrow: 1, + justifyContent: 'center', + alignItems: 'center', +}); + +const ResizeIcon = glamorous.span({ + height: '2px', + width: '20px', + borderTop: '1px solid rgba(0, 0, 0, 0.3)', + borderBottom: '1px solid rgba(0, 0, 0, 0.3)', +}); + +class Resizer extends React.Component { + + static propTypes = { + onResize: PropTypes.func, + } + + constructor(...args) { + super(...args); + + this.handleMouseDown = this.handleMouseDown.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + + this.notifyResize = throttleAnimation(this.notifyResize.bind(this)); + } + + handleMouseDown(event) { + if (event.buttons & 1) { // eslint-disable-line no-bitwise + this.startY = event.pageY; + + document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mouseup', this.handleMouseUp); + event.preventDefault(); + } + } + + handleMouseMove(event) { + const {onResize} = this.props; + + event.preventDefault(); + + if (is_defined(onResize)) { + const diffY = event.pageY - this.startY; + this.startY = event.pageY; + this.notifyResize(diffY); // difference since last notification + } + } + + notifyResize(diffY) { + const {onResize} = this.props; + onResize(diffY); + } + + handleMouseUp(event) { + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); + event.preventDefault(); + } + + componentWillUnmount() { + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); + } + + render() { + return ( + + + + ); + } +} + +export default Resizer; + +// vim: set ts=2 sw=2 tw=80: diff --git a/ng/src/web/components/sortable/row.js b/ng/src/web/components/sortable/row.js index 6a9fd47f7a..f444fca467 100644 --- a/ng/src/web/components/sortable/row.js +++ b/ng/src/web/components/sortable/row.js @@ -26,41 +26,81 @@ import glamorous from 'glamorous'; import {Droppable} from 'react-beautiful-dnd'; -import PropTypes from '../../utils/proptypes.js'; +import {is_defined} from 'gmp/utils'; -const GridRow = glamorous.div({ +import PropTypes from '../../utils/proptypes'; + +import Resizer from './resizer'; + +const MIN_HEIGHT = 50; + +const GridRow = glamorous.div('grid-row', { display: 'flex', - margin: '8px 0px', - minHeight: '50px', -}, ({isDraggingOver}) => ({ + minHeight: `${MIN_HEIGHT}px`, +}, ({isDraggingOver, height}) => ({ background: isDraggingOver ? 'lightblue' : 'none', + height, })); -const Row = ({ - children, - dropDisabled, - id, -}) => ( - - {(provided, snapshot) => ( - - {children} - {provided.placeholder} - - )} - -); +class Row extends React.Component { + constructor(...args) { + super(...args); + + this.handleResize = this.handleResize.bind(this); + } + + handleResize(diffY) { + const {onResize} = this.props; + + if (is_defined(onResize)) { + const box = this.row.getBoundingClientRect(); + const height = box.height + diffY; + + if (height > MIN_HEIGHT) { + onResize(height); + } + } + } + + render() { + const { + children, + dropDisabled, + id, + height, + } = this.props; + return ( + + + {(provided, snapshot) => ( + { + this.row = ref; + provided.innerRef(ref); + }} + isDraggingOver={snapshot.isDraggingOver} + height={height} + > + {children} + {provided.placeholder} + + )} + + + + ); + } +} Row.propTypes = { dropDisabled: PropTypes.bool, + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), id: PropTypes.string.isRequired, + onResize: PropTypes.func, }; export default Row; diff --git a/ng/src/web/stories/sortable/index.js b/ng/src/web/stories/sortable/index.js index 36eb5c95f6..77d209624b 100644 --- a/ng/src/web/stories/sortable/index.js +++ b/ng/src/web/stories/sortable/index.js @@ -22,13 +22,54 @@ */ import React from 'react'; -import glamorous from 'glamorous'; +import glamorous, {Div} from 'glamorous'; import {storiesOf} from '@storybook/react'; import PropTypes from 'web/utils/proptypes'; import Grid, {createItem, createRow} from 'web/components/sortable/grid.js'; +import Resizer from 'web/components/sortable/resizer'; + +class ResizeContainer extends React.Component { + + constructor(...args) { + super(...args); + + this.state = { + height: '100px', + }; + + this.handleResize = this.handleResize.bind(this); + } + + handleResize(diffY) { + const box = this.div.getBoundingClientRect(); + const height = box.height + diffY; + + if (height > 20) { + this.setState({height}); + } + } + + render() { + const {height} = this.state; + return ( +
+
this.div = ref} + /> + +
+ ); + } +} class ItemController extends React.Component { @@ -90,7 +131,7 @@ storiesOf('Sortable/Grid', module) }) .add('max 5 items per row', () => { const items = [ - createRow(getItems(0, 3)), + {...createRow(getItems(0, 3)), height: '400px'}, createRow(getItems(1, 5)), ]; return ( @@ -104,4 +145,8 @@ storiesOf('Sortable/Grid', module) ); }); +storiesOf('Sortable/Resizer', module) + .add('default', () => ( + + )); // vim: set ts=2 sw=2 tw=80: