diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index e0e897055..1d61bae36 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -3,43 +3,23 @@ import classnames from 'classnames' import uuid from 'uuid' import _ from 'lodash' -import ResizeDivision from 'src/shared/components/ResizeDivision' +import ResizeHalf from 'src/shared/components/ResizeHalf' +import ResizeHandle from 'src/shared/components/ResizeHandle' import {ErrorHandling} from 'src/shared/decorators/errors' -import { - MIN_DIVISIONS, - HANDLE_HORIZONTAL, - HANDLE_VERTICAL, -} from 'src/shared/constants/' - -const initialDragEvent = { - percentX: 0, - percentY: 0, - mouseX: null, - mouseY: null, -} +import {HANDLE_HORIZONTAL, HANDLE_VERTICAL} from 'src/shared/constants/index' interface State { - activeHandleID: string - divisions: DivisionState[] - dragDirection: string - dragEvent: any -} - -interface Division { - name?: string - render: () => ReactElement - minPixels?: number -} - -interface DivisionState extends Division { - id: string - size: number - minPixels?: number + topPercent: number + bottomPercent: number + isDragging: boolean } interface Props { - divisions: Division[] - orientation: string + topHalf: () => ReactElement + topMinPixels: number + bottomHalf: () => ReactElement + bottomMinPixels: number + orientation?: string containerClass: string } @@ -56,10 +36,9 @@ class Resizer extends Component { constructor(props) { super(props) this.state = { - activeHandleID: null, - divisions: this.initialDivisions, - dragEvent: initialDragEvent, - dragDirection: '', + topPercent: 0.5, + bottomPercent: 0.5, + isDragging: false, } } @@ -102,15 +81,14 @@ class Resizer extends Component { } public render() { - const {activeHandleID, divisions} = this.state - const {orientation} = this.props - - if (divisions.length < MIN_DIVISIONS) { - console.error( - `There must be at least ${MIN_DIVISIONS}' divisions in Resizer` - ) - return - } + const {isDragging, topPercent, bottomPercent} = this.state + const { + topHalf, + topMinPixels, + bottomHalf, + bottomMinPixels, + orientation, + } = this.props return (
{ onMouseMove={this.handleDrag} ref={r => (this.containerRef = r)} > - {divisions.map((d, i) => ( - 0} - minPixels={d.minPixels} - orientation={orientation} - activeHandleID={activeHandleID} - onHandleStartDrag={this.handleStartDrag} - maxPercent={this.maximumHeightPercent} - render={this.props.divisions[i].render} - /> - ))} + + +
) } @@ -149,39 +129,23 @@ class Resizer extends Component { private get className(): string { const {orientation, containerClass} = this.props - const {activeHandleID} = this.state return classnames(`resize--container ${containerClass}`, { - 'resize--dragging': activeHandleID, horizontal: orientation === HANDLE_HORIZONTAL, vertical: orientation === HANDLE_VERTICAL, }) } - private get initialDivisions() { - const {divisions} = this.props - - const size = 1 / divisions.length - - return divisions.map(d => ({ - ...d, - id: uuid.v4(), - size, - minPixels: d.minPixels || 0, - })) - } - - private handleStartDrag = (activeHandleID, e: MouseEvent) => { - const dragEvent = this.mousePosWithinContainer(e) - this.setState({activeHandleID, dragEvent}) + private handleStartDrag = () => { + this.setState({isDragging: true}) } private handleStopDrag = () => { - this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + this.setState({isDragging: false}) } private handleMouseLeave = () => { - this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + this.setState({isDragging: false}) } private mousePosWithinContainer = (e: MouseEvent) => { @@ -242,24 +206,6 @@ class Resizer extends Component { return yMinPixels / height } - private get maximumHeightPercent(): number { - if (!this.containerRef) { - return 1 - } - - const {divisions} = this.state - const {height} = this.containerRef.getBoundingClientRect() - - const totalMinPixels = divisions.reduce( - (acc, div) => acc + div.minPixels, - 0 - ) - - const maximumPixels = height - totalMinPixels - - return this.minPercentY(maximumPixels) - } - private handleDrag = (e: MouseEvent) => { const {activeHandleID} = this.state if (!activeHandleID) { @@ -283,143 +229,6 @@ class Resizer extends Component { private isAtMinHeight = (division: DivisionState): boolean => { return division.size <= this.minPercentY(division.minPixels) } - - private get move() { - const {activeHandleID} = this.state - - const activePosition = _.findIndex( - this.state.divisions, - d => d.id === activeHandleID - ) - - return { - up: this.up(activePosition), - down: this.down(activePosition), - left: this.left(activePosition), - right: this.right(activePosition), - } - } - - private up = activePosition => () => { - const divisions = this.state.divisions.map((d, i, divs) => { - const before = i < activePosition - const current = i === activePosition - const after = i === activePosition + 1 - - if (before) { - const below = divs[i + 1] - const aboveCurrent = i === activePosition - 1 - - if (this.isAtMinHeight(below) || aboveCurrent) { - return {...d, size: this.shorter(d.size)} - } - } - - if (current) { - const stayStill = divs.every((div, idx) => { - if (idx >= i) { - return true - } - - return this.isAtMinHeight(div) - }) - - if (stayStill) { - return {...d} - } - - return {...d, size: this.taller(d.size)} - } - - if (after) { - return {...d} - } - - return {...d} - }) - - this.setState({divisions: this.cleanDivisions(divisions)}) - } - - private down = activePosition => () => { - const divisions = this.state.divisions.map((d, i, divs) => { - const before = i === activePosition - 1 - const current = i === activePosition - const after = i > activePosition - - if (before) { - return {...d, size: this.taller(d.size)} - } - - if (current) { - return {...d, size: this.shorter(d.size)} - } - - if (after) { - const above = divs[i - 1] - - if (this.isAtMinHeight(above)) { - return {...d, size: this.shorter(d.size)} - } - } - - return {...d} - }) - - this.setState({divisions: this.cleanDivisions(divisions)}) - } - - private left = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { - const before = i === activePosition - 1 - const active = i === activePosition - - if (before) { - return {...d, size: d.size - this.percentChangeX} - } else if (active) { - return {...d, size: d.size + this.percentChangeX} - } - - return d - }) - - this.setState({divisions}) - } - - private right = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { - const before = i === activePosition - 1 - const active = i === activePosition - - if (before) { - return {...d, size: d.size + this.percentChangeX} - } else if (active) { - return {...d, size: d.size - this.percentChangeX} - } - - return d - }) - - this.setState({divisions}) - } - - private enforceSize = (size, minPixels): number => { - const minPercent = this.minPercent(minPixels) - - let enforcedSize = size - if (size < minPercent) { - enforcedSize = minPercent - } - - return enforcedSize - } - - private cleanDivisions = divisions => { - return divisions.map(d => { - const size = this.enforceSize(d.size, d.minPixels) - return {...d, size} - }) - } } export default Resizer diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 510d897d4..a00c655e0 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -72,7 +72,8 @@ class Division extends PureComponent { const {orientation, maxPercent, minPixels, size} = this.props const sizePercent = `${size * HUNDRED}%` - const max = `${maxPercent * HUNDRED}%` + // const max = `${maxPercent * HUNDRED}%` + const max = '100%' if (orientation === HANDLE_VERTICAL) { return { diff --git a/ui/src/shared/components/ResizeHalf.tsx b/ui/src/shared/components/ResizeHalf.tsx new file mode 100644 index 000000000..e1bbc00f7 --- /dev/null +++ b/ui/src/shared/components/ResizeHalf.tsx @@ -0,0 +1,58 @@ +import React, {PureComponent, ReactElement} from 'react' +import classnames from 'classnames' + +import { + HANDLE_VERTICAL, + HANDLE_HORIZONTAL, + HUNDRED, +} from 'src/shared/constants/' + +interface Props { + percent: number + minPixels: number + render: () => ReactElement + orientation: string +} + +class ResizerHalf extends PureComponent { + public render() { + const {render} = this.props + + return ( +
+ {render()} +
+ ) + } + + private get style() { + const {orientation, minPixels, percent} = this.props + + const size = `${percent * HUNDRED}%` + + if (orientation === HANDLE_VERTICAL) { + return { + top: '0', + width: size, + minWidth: minPixels, + } + } + + return { + left: '0', + height: size, + minHeight: minPixels, + } + } + + private get className(): string { + const {orientation} = this.props + + return classnames('resizer--half', { + vertical: orientation === HANDLE_VERTICAL, + horizontal: orientation === HANDLE_HORIZONTAL, + }) + } +} + +export default ResizerHalf diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx new file mode 100644 index 000000000..cc2b93f36 --- /dev/null +++ b/ui/src/shared/components/Threesizer.tsx @@ -0,0 +1,456 @@ +import React, {Component, ReactElement, MouseEvent} from 'react' +import classnames from 'classnames' +import uuid from 'uuid' +import _ from 'lodash' + +import ResizeDivision from 'src/shared/components/ResizeDivision' +import {ErrorHandling} from 'src/shared/decorators/errors' +import { + MIN_DIVISIONS, + HANDLE_HORIZONTAL, + HANDLE_VERTICAL, +} from 'src/shared/constants/' + +const initialDragEvent = { + percentX: 0, + percentY: 0, + mouseX: null, + mouseY: null, +} + +interface State { + activeHandleID: string + divisions: DivisionState[] + dragDirection: string + dragEvent: any +} + +interface Division { + name?: string + render: () => ReactElement + minPixels?: number +} + +interface DivisionState extends Division { + id: string + size: number + minPixels?: number +} + +interface Props { + divisions: Division[] + orientation: string + containerClass: string +} + +@ErrorHandling +class Resizer extends Component { + public static defaultProps: Partial = { + orientation: HANDLE_HORIZONTAL, + } + + private containerRef: HTMLElement + private percentChangeX: number = 0 + private percentChangeY: number = 0 + + constructor(props) { + super(props) + this.state = { + activeHandleID: null, + divisions: this.initialDivisions, + dragEvent: initialDragEvent, + dragDirection: '', + } + } + + public componentDidUpdate(__, prevState) { + const {dragEvent} = this.state + const {orientation} = this.props + + if (_.isEqual(dragEvent, prevState.dragEvent)) { + return + } + + this.percentChangeX = this.pixelsToPercentX( + prevState.dragEvent.mouseX, + dragEvent.mouseX + ) + + this.percentChangeY = this.pixelsToPercentY( + prevState.dragEvent.mouseY, + dragEvent.mouseY + ) + + if (orientation === HANDLE_VERTICAL) { + const left = dragEvent.percentX < prevState.dragEvent.percentX + + if (left) { + return this.move.left() + } + + return this.move.right() + } + + const up = dragEvent.percentY < prevState.dragEvent.percentY + const down = dragEvent.percentY > prevState.dragEvent.percentY + + if (up) { + return this.move.up() + } else if (down) { + return this.move.down() + } + } + + public render() { + const {activeHandleID, divisions} = this.state + const {orientation} = this.props + + if (divisions.length < MIN_DIVISIONS) { + console.error( + `There must be at least ${MIN_DIVISIONS}' divisions in Resizer` + ) + return + } + + return ( +
(this.containerRef = r)} + > + {divisions.map((d, i) => ( + 0} + minPixels={d.minPixels} + orientation={orientation} + activeHandleID={activeHandleID} + onHandleStartDrag={this.handleStartDrag} + maxPercent={this.maximumHeightPercent} + render={this.props.divisions[i].render} + /> + ))} +
+ ) + } + + private minPercent = (minPixels: number): number => { + if (this.props.orientation === HANDLE_VERTICAL) { + return this.minPercentX(minPixels) + } + + return this.minPercentY(minPixels) + } + + private get className(): string { + const {orientation, containerClass} = this.props + const {activeHandleID} = this.state + + return classnames(`resize--container ${containerClass}`, { + 'resize--dragging': activeHandleID, + horizontal: orientation === HANDLE_HORIZONTAL, + vertical: orientation === HANDLE_VERTICAL, + }) + } + + private get initialDivisions() { + const {divisions} = this.props + + const size = 1 / divisions.length + + return divisions.map(d => ({ + ...d, + id: uuid.v4(), + size, + minPixels: d.minPixels || 0, + })) + } + + private handleStartDrag = (activeHandleID, e: MouseEvent) => { + const dragEvent = this.mousePosWithinContainer(e) + this.setState({activeHandleID, dragEvent}) + } + + private handleStopDrag = () => { + this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + } + + private handleMouseLeave = () => { + this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + } + + private mousePosWithinContainer = (e: MouseEvent) => { + const {pageY, pageX} = e + const {top, left, width, height} = this.containerRef.getBoundingClientRect() + + const mouseX = pageX - left + const mouseY = pageY - top + + const percentX = mouseX / width + const percentY = mouseY / height + + return { + mouseX, + mouseY, + percentX, + percentY, + } + } + + private pixelsToPercentX = (startValue, endValue) => { + if (!startValue) { + return 0 + } + + const delta = startValue - endValue + const {width} = this.containerRef.getBoundingClientRect() + + return Math.abs(delta / width) + } + + private pixelsToPercentY = (startValue, endValue) => { + if (!startValue) { + return 0 + } + + const delta = startValue - endValue + const {height} = this.containerRef.getBoundingClientRect() + + return Math.abs(delta / height) + } + + private minPercentX = (xMinPixels: number): number => { + if (!this.containerRef) { + return 0 + } + const {width} = this.containerRef.getBoundingClientRect() + + return xMinPixels / width + } + + private minPercentY = (yMinPixels: number): number => { + if (!this.containerRef) { + return 0 + } + + const {height} = this.containerRef.getBoundingClientRect() + return yMinPixels / height + } + + private get maximumHeightPercent(): number { + if (!this.containerRef) { + return 1 + } + + const {divisions} = this.state + const {height} = this.containerRef.getBoundingClientRect() + + const totalMinPixels = divisions.reduce( + (acc, div) => acc + div.minPixels, + 0 + ) + + const maximumPixels = height - totalMinPixels + + return this.minPercentY(maximumPixels) + } + + private handleDrag = (e: MouseEvent) => { + const {activeHandleID} = this.state + if (!activeHandleID) { + return + } + + const dragEvent = this.mousePosWithinContainer(e) + this.setState({dragEvent}) + } + + private taller = (size: number): number => { + const newSize = size + this.percentChangeY + return Number(newSize.toFixed(3)) + } + + private shorter = (size: number): number => { + const newSize = size - this.percentChangeY + return Number(newSize.toFixed(3)) + } + + private isAtMinHeight = (division: DivisionState): boolean => { + return division.size <= this.minPercentY(division.minPixels) + } + + private get move() { + const {activeHandleID} = this.state + + const activePosition = _.findIndex( + this.state.divisions, + d => d.id === activeHandleID + ) + + return { + up: this.up(activePosition), + down: this.down(activePosition), + left: this.left(activePosition), + right: this.right(activePosition), + } + } + + private up = activePosition => () => { + const divisions = this.state.divisions.map((d, i, divs) => { + const before = i < activePosition + const current = i === activePosition + + if (before) { + const below = divs[i + 1] + const aboveCurrent = i === activePosition - 1 + + const belowIsCurrent = below.id === divs[activePosition].id + + if (belowIsCurrent) { + return {...d} + } + + if (this.isAtMinHeight(below) || aboveCurrent) { + const size = this.shorter(d.size) + if (size < 0) { + debugger + } + + return {...d, size} + } + } + + if (current) { + const stayStill = divs.every((div, idx) => { + if (idx >= i) { + return true + } + + return this.isAtMinHeight(div) + }) + + if (stayStill) { + return {...d} + } + + return {...d, size: this.taller(d.size)} + } + + return {...d} + }) + + this.setState({divisions}) + } + + private down = activePosition => () => { + const divisions = this.state.divisions.map((d, i, divs) => { + const before = i === activePosition - 1 + const current = i === activePosition + const after = i > activePosition + + if (before) { + return {...d, size: this.taller(d.size)} + } + + if (current) { + return {...d, size: this.shorter(d.size)} + } + + if (after) { + const above = divs[i - 1] + + if (this.isAtMinHeight(d)) { + return {...d} + } + + if (this.isAtMinHeight(above)) { + return {...d, size: this.shorter(d.size)} + } + } + + return {...d} + }) + + this.setState({divisions}) + } + + private left = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size - this.percentChangeX} + } else if (active) { + return {...d, size: d.size + this.percentChangeX} + } + + return d + }) + + this.setState({divisions}) + } + + private right = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size + this.percentChangeX} + } else if (active) { + return {...d, size: d.size - this.percentChangeX} + } + + return d + }) + + this.setState({divisions}) + } + + private enforceSize = (size, minPixels): number => { + const minPercent = this.minPercent(minPixels) + + let enforcedSize = size + if (size < minPercent) { + enforcedSize = minPercent + } + + return enforcedSize + } + + private cleanDivisions = divisions => { + const minSizes = divisions.map(d => { + const size = this.enforceSize(d.size, d.minPixels) + return {...d, size} + }) + + const sumSizes = minSizes.reduce((acc, div, i) => { + if (i <= divisions.length - 1) { + return acc + div.size + } + + return acc + }, 0) + + const under100percent = 1 - sumSizes > 0 + const over100percent = 1 - sumSizes < 0 + + if (under100percent) { + minSizes[divisions.length - 1].size += Math.abs(1 - sumSizes) + } + + if (over100percent) { + minSizes[divisions.length - 1].size -= Math.abs(1 - sumSizes) + } + + return minSizes + } +} + +export default Resizer diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index e66b100c8..8ae32df58 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -52,9 +52,18 @@ $resizer-color-kapacitor: $c-rainforest; height: 100%; } - .resizer--division { + .resizer--division:nth-child(1) { border-color: #0f0; } + .resizer--division:nth-child(2) { + border-color: #0ff; + } + .resizer--division:nth-child(3) { + border-color: #00f; + } + .resizer--division:nth-child(4) { + border-color: #f0f; + } } .resizer--full-size {