WIP Split resizer into 2 part "Resizer" and the "Threesizer"

pull/3374/head
Alex P 2018-05-02 09:29:34 -07:00 committed by Andrew Watkins
parent 4f0988cc21
commit 0311f299af
5 changed files with 569 additions and 236 deletions

View File

@ -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<any>
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<any>
topMinPixels: number
bottomHalf: () => ReactElement<any>
bottomMinPixels: number
orientation?: string
containerClass: string
}
@ -56,10 +36,9 @@ class Resizer extends Component<Props, State> {
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<Props, State> {
}
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 (
<div
@ -120,21 +98,23 @@ class Resizer extends Component<Props, State> {
onMouseMove={this.handleDrag}
ref={r => (this.containerRef = r)}
>
{divisions.map((d, i) => (
<ResizeDivision
key={d.id}
id={d.id}
name={d.name}
size={d.size}
draggable={i > 0}
minPixels={d.minPixels}
orientation={orientation}
activeHandleID={activeHandleID}
onHandleStartDrag={this.handleStartDrag}
maxPercent={this.maximumHeightPercent}
render={this.props.divisions[i].render}
/>
))}
<ResizeHalf
percent={topPercent}
minPixels={topMinPixels}
render={topHalf}
orientation={orientation}
/>
<ResizeHandle
onStartDrag={this.handleStartDrag}
isDragging={isDragging}
orientation={orientation}
/>
<ResizeHalf
percent={bottomPercent}
minPixels={bottomMinPixels}
render={bottomHalf}
orientation={orientation}
/>
</div>
)
}
@ -149,39 +129,23 @@ class Resizer extends Component<Props, State> {
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<HTMLElement>) => {
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<HTMLElement>) => {
@ -242,24 +206,6 @@ class Resizer extends Component<Props, State> {
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<HTMLElement>) => {
const {activeHandleID} = this.state
if (!activeHandleID) {
@ -283,143 +229,6 @@ class Resizer extends Component<Props, State> {
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

View File

@ -72,7 +72,8 @@ class Division extends PureComponent<Props> {
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 {

View File

@ -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<any>
orientation: string
}
class ResizerHalf extends PureComponent<Props> {
public render() {
const {render} = this.props
return (
<div className={this.className} style={this.style}>
{render()}
</div>
)
}
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

View File

@ -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<any>
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<Props, State> {
public static defaultProps: Partial<Props> = {
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 (
<div
className={this.className}
onMouseLeave={this.handleMouseLeave}
onMouseUp={this.handleStopDrag}
onMouseMove={this.handleDrag}
ref={r => (this.containerRef = r)}
>
{divisions.map((d, i) => (
<ResizeDivision
key={d.id}
id={d.id}
name={d.name}
size={d.size}
draggable={i > 0}
minPixels={d.minPixels}
orientation={orientation}
activeHandleID={activeHandleID}
onHandleStartDrag={this.handleStartDrag}
maxPercent={this.maximumHeightPercent}
render={this.props.divisions[i].render}
/>
))}
</div>
)
}
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<HTMLElement>) => {
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<HTMLElement>) => {
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<HTMLElement>) => {
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

View File

@ -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 {