From c8a0fbbb96fdf29e4143b913f64c5a0792091395 Mon Sep 17 00:00:00 2001 From: Mat Brown Date: Sun, 27 May 2018 06:56:53 -0400 Subject: [PATCH] Use generic higher-order component for drag-to-resize vertical divider Applies new `resizableFlex` higher-order component to the `` component to control resizing of the flex regions containing the editors column and output respectively. Fully removes the tightly-coupled flex resizing support from Redux and React infrastructure. Differences between calculating flex for row and column flex directions are encapsulated in a pair of adapters. Flex direction-specific property access now in adapter Fix adapter usage Use generic HOC for drag-to-resize editors column and preview Only works in one direction because of pointer capture Restore start/stop drag Fix lint Remove explicit Redux-level support for drag-to-resize Removes support for resizing the center vertical divider, now that this functionality has been migrated to the resizeable flex module. One dangling rowflex reference --- src/actions/index.js | 2 - src/actions/ui.js | 4 - src/components/Workspace.jsx | 46 ++++------- src/containers/Workspace.js | 13 +--- src/reducers/ui.js | 17 ----- src/util/resizableFlex.js | 56 +++++++++++--- src/util/resize.js | 143 ----------------------------------- test/unit/reducers/ui.js | 20 ----- 8 files changed, 62 insertions(+), 239 deletions(-) delete mode 100644 src/util/resize.js diff --git a/src/actions/index.js b/src/actions/index.js index b9ef87fa8d..ccaad5a2a1 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -23,7 +23,6 @@ import { import { focusLine, editorFocusedRequestedLine, - dragColumnDivider, startDragColumnDivider, stopDragColumnDivider, notificationTriggered, @@ -87,7 +86,6 @@ export { toggleComponent, focusLine, editorFocusedRequestedLine, - dragColumnDivider, startDragColumnDivider, stopDragColumnDivider, notificationTriggered, diff --git a/src/actions/ui.js b/src/actions/ui.js index c341c401cc..a0b1859270 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -12,10 +12,6 @@ export const editorFocusedRequestedLine = createAction( 'EDITOR_FOCUSED_REQUESTED_LINE', ); -export const dragColumnDivider = createAction( - 'DRAG_COLUMN_DIVIDER', -); - export const startDragColumnDivider = createAction( 'START_DRAG_COLUMN_DIVIDER', ); diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx index 5cc4ae6ce0..7cacf45d23 100644 --- a/src/components/Workspace.jsx +++ b/src/components/Workspace.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import {DraggableCore} from 'react-draggable'; import bindAll from 'lodash-es/bindAll'; import isNull from 'lodash-es/isNull'; @@ -8,7 +9,6 @@ import partial from 'lodash-es/partial'; import {t} from 'i18next'; import classnames from 'classnames'; -import {getNodeWidth, getNodeWidths} from '../util/resize'; import {getQueryParameters, setQueryParameters} from '../util/queryParams'; import {dehydrateProject, rehydrateProject} from '../clients/localStorage'; @@ -29,9 +29,6 @@ export default class Workspace extends React.Component { this, '_handleUnload', '_handleClickInstructionsBar', - '_handleDividerDrag', - '_storeDividerRef', - '_storeColumnRef', ); this.columnRefs = [null, null]; } @@ -104,55 +101,39 @@ export default class Workspace extends React.Component { ); } - _storeColumnRef(index, column) { - this.columnRefs[index] = column; - } - - _storeDividerRef(divider) { - this.dividerRef = divider; - } - - _handleDividerDrag(_, {deltaX, lastX, x}) { - const {onDragColumnDivider} = this.props; - onDragColumnDivider({ - columnWidths: getNodeWidths(this.columnRefs), - dividerWidth: getNodeWidth(this.dividerRef), - deltaX, - lastX, - x, - }); - } - _renderEnvironment() { const { currentProject, + resizableFlexGrow, + resizableFlexRefs, + onResizableFlexDividerDrag, onStartDragColumnDivider, onStopDragColumnDivider, - rowsFlex, } = this.props; if (isNull(currentProject)) { return ; } + const [_handleEditorsRef, _handleOutputRef] = resizableFlexRefs; + return (
); @@ -178,10 +159,11 @@ export default class Workspace extends React.Component { Workspace.propTypes = { currentProject: PropTypes.object, isEditingInstructions: PropTypes.bool.isRequired, - rowsFlex: PropTypes.array.isRequired, + resizableFlexGrow: ImmutablePropTypes.list.isRequired, + resizableFlexRefs: PropTypes.array.isRequired, onApplicationLoaded: PropTypes.func.isRequired, onComponentToggle: PropTypes.func.isRequired, - onDragColumnDivider: PropTypes.func.isRequired, + onResizableFlexDividerDrag: PropTypes.func.isRequired, onStartDragColumnDivider: PropTypes.func.isRequired, onStopDragColumnDivider: PropTypes.func.isRequired, }; diff --git a/src/containers/Workspace.js b/src/containers/Workspace.js index 995feb5ffa..efb31ed5c0 100644 --- a/src/containers/Workspace.js +++ b/src/containers/Workspace.js @@ -3,18 +3,17 @@ import {connect} from 'react-redux'; import Workspace from '../components/Workspace'; import {getCurrentProject, isEditingInstructions} from '../selectors'; import { - dragColumnDivider, - startDragColumnDivider, - stopDragColumnDivider, toggleComponent, applicationLoaded, + startDragColumnDivider, + stopDragColumnDivider, } from '../actions'; +import resizableFlex from '../util/resizableFlex'; function mapStateToProps(state) { return { currentProject: getCurrentProject(state), isEditingInstructions: isEditingInstructions(state), - rowsFlex: state.getIn(['ui', 'workspace', 'rowFlex']).toJS(), }; } @@ -28,10 +27,6 @@ function mapDispatchToProps(dispatch) { dispatch(toggleComponent(projectKey, componentName)); }, - onDragColumnDivider(payload) { - dispatch(dragColumnDivider(payload)); - }, - onStartDragColumnDivider() { dispatch(startDragColumnDivider()); }, @@ -45,4 +40,4 @@ function mapDispatchToProps(dispatch) { export default connect( mapStateToProps, mapDispatchToProps, -)(Workspace); +)(resizableFlex(2)(Workspace)); diff --git a/src/reducers/ui.js b/src/reducers/ui.js index e6c4627e7f..0aaf340a21 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -1,10 +1,6 @@ import Immutable from 'immutable'; -import {updateWorkspaceRowFlex} from '../util/resize'; - -const DEFAULT_ROW_FLEX = new Immutable.List(['1', '1']); export const DEFAULT_WORKSPACE = new Immutable.Map({ - rowFlex: DEFAULT_ROW_FLEX, isDraggingColumnDivider: false, isEditingInstructions: false, }); @@ -53,13 +49,6 @@ export default function ui(stateIn, action) { case 'PROJECT_CREATED': return state.set('workspace', DEFAULT_WORKSPACE); - case 'HIDE_COMPONENT': - case 'UNHIDE_COMPONENT': - if (action.payload.componentName === 'output') { - return state.setIn(['workspace', 'rowFlex'], DEFAULT_ROW_FLEX); - } - return state; - case 'UPDATE_PROJECT_SOURCE': return state.setIn(['editors', 'typing'], true); @@ -87,12 +76,6 @@ export default function ui(stateIn, action) { case 'EDITOR_FOCUSED_REQUESTED_LINE': return state.setIn(['editors', 'requestedFocusedLine'], null); - case 'DRAG_COLUMN_DIVIDER': - return state.updateIn(['workspace', 'rowFlex'], (prevFlex) => { - const newFlex = updateWorkspaceRowFlex(action.payload); - return newFlex ? Immutable.fromJS(newFlex) : prevFlex; - }); - case 'START_DRAG_COLUMN_DIVIDER': return state.setIn(['workspace', 'isDraggingColumnDivider'], true); diff --git a/src/util/resizableFlex.js b/src/util/resizableFlex.js index a37d318d81..20db5c1b53 100644 --- a/src/util/resizableFlex.js +++ b/src/util/resizableFlex.js @@ -9,6 +9,33 @@ import times from 'lodash-es/times'; import {makeGetResizableFlexGrow} from '../selectors'; import {updateResizableFlex} from '../actions'; +const directionAdapters = { + column: { + getCurrentSize(element) { + return element.offsetHeight; + }, + + getDesiredSize(element, {y}) { + return y - element.offsetTop; + }, + }, + + row: { + getCurrentSize(element) { + return element.offsetWidth; + }, + + getDesiredSize(element, {x}) { + return x - element.offsetLeft; + }, + }, +}; + +function directionAdapterFor({parentNode}) { + const flexDirection = getComputedStyle(parentNode)['flex-direction']; + return directionAdapters[flexDirection]; +} + function calculateFlexGrowAfterDrag( { currentFlexGrow: currentBeforeFlexGrow, @@ -60,21 +87,24 @@ export default function resizableFlex(size) { const [{current: before}, {current: after}] = at(regions, [beforeIndex, afterIndex]); + const {getCurrentSize, getDesiredSize} = + directionAdapterFor(before); + const [desiredBeforeFlexGrow, desiredAfterFlexGrow] = calculateFlexGrowAfterDrag( { currentFlexGrow: Number( getComputedStyle(before)['flex-grow'], ), - currentSize: before.offsetHeight, - desiredSize: payload.y - before.offsetTop, + currentSize: getCurrentSize(before), + desiredSize: getDesiredSize(before, payload), initialMainSize: initialMainSizes[beforeIndex], }, { currentFlexGrow: Number( getComputedStyle(after)['flex-grow'], ), - currentSize: after.offsetHeight, + currentSize: getCurrentSize(after), initialMainSize: initialMainSizes[afterIndex], }, ); @@ -90,18 +120,20 @@ export default function resizableFlex(size) { resizableFlexRefs: map( regions, - (region, index) => (ref) => { - region.current = ref; - if (isNull(ref)) { + (region, index) => (element) => { + region.current = element; + if (isNull(element)) { initialMainSizes[index] = null; return; } - const flexGrowWas = ref.style.flexGrow; - const flexShrinkWas = ref.style.flexShrink; - ref.style.flexGrow = ref.style.flexShrink = '0'; - initialMainSizes[index] = ref.offsetHeight; - ref.style.flexGrow = flexGrowWas; - ref.style.flexShrink = flexShrinkWas; + + const flexGrowWas = element.style.flexGrow; + const flexShrinkWas = element.style.flexShrink; + element.style.flexGrow = element.style.flexShrink = '0'; + initialMainSizes[index] = directionAdapterFor(element). + getCurrentSize(element); + element.style.flexGrow = flexGrowWas; + element.style.flexShrink = flexShrinkWas; }, ), diff --git a/src/util/resize.js b/src/util/resize.js deleted file mode 100644 index f6033a2346..0000000000 --- a/src/util/resize.js +++ /dev/null @@ -1,143 +0,0 @@ -export function getNodeWidth(node) { - const {minWidth} = window.getComputedStyle(node); - return { - width: node.offsetWidth, - minWidth: parseInt(minWidth.replace('px', ''), 10), - }; -} - -export function getNodeWidths(refs) { - return refs.map(node => node ? getNodeWidth(node) : null); -} - -export function getNodeHeights(refs) { - return refs.map((node) => { - if (node) { - const {minHeight} = window.getComputedStyle(node); - return { - height: node.offsetHeight, - minHeight: parseInt(minHeight.replace('px', ''), 10), - }; - } - return null; - }); -} - -function isSecondDividerPushingFirstDivider( - [ - {size: size0, minSize: contentMinSize0}, - {size: size1, minSize: contentMinSize1}, - ], - deltaPosition, -) { - return size0 > contentMinSize0 && - size1 === contentMinSize1 && - deltaPosition < 0; -} - -function constrainPosition( - [,, {size, minSize}], - lastPosition, - deltaPosition, - offset, -) { - const maxDeltaY = size - minSize; - return lastPosition + Math.min(deltaPosition, maxDeltaY) - offset; -} - -function columnToContent(column) { - return column ? {size: column.width, minSize: column.minWidth} : null; -} - -function editorToContent(editor) { - return editor ? {size: editor.height, minSize: editor.minHeight} : null; -} - -export function updateEditorColumnFlex({ - index, - dividerHeights, - editorHeights, - deltaY, - lastY, - y, -}) { - return updateFlex({ - index, - contentSizes: editorHeights.map(editorToContent), - dividerSizes: dividerHeights.map(editorToContent), - deltaPosition: deltaY, - lastPosition: lastY, - position: y, - }); -} - -export function updateWorkspaceRowFlex({ - columnWidths, - dividerWidth, - deltaX, - lastX, - x, -}) { - return updateFlex({ - index: 0, - contentSizes: columnWidths.map(columnToContent), - dividerSizes: [columnToContent(dividerWidth)], - deltaPosition: deltaX, - lastPosition: lastX, - position: x, - }); -} - -function updateFlex({ - deltaPosition, - dividerSizes: [{minSize: dividerMinSize0}], - contentSizes, - index, - lastPosition, - position, -}) { - const [ - {size: contentSize0}, - {size: contentSize1}, - ] = contentSizes; - const contentSize2 = contentSizes[2] ? contentSizes[2].size : 0; - - if (index === 0) { - return [ - `0 1 ${position}px`, - '1', - contentSize2 ? `0 1 ${contentSize2}px` : '1', - ]; - } - const distanceToTopOfContent1 = dividerMinSize0 + contentSize0; - const projectedSizeOfContent1 = position - distanceToTopOfContent1; - const contentMinSize2 = contentSizes[2].minSize; - if ( - isSecondDividerPushingFirstDivider(contentSizes, deltaPosition) - ) { - return [ - `0 1 ${contentSize0 + deltaPosition}px`, - `0 1 ${projectedSizeOfContent1}px`, - '1', - ]; - } else if (contentSize2 > contentMinSize2) { - const constrainedSizeOfContent1 = constrainPosition( - contentSizes, - lastPosition, - deltaPosition, - distanceToTopOfContent1, - ); - return [ - `0 1 ${contentSize0}px`, - `0 1 ${constrainedSizeOfContent1}px`, - '1', - ]; - } else if (deltaPosition < 0 && projectedSizeOfContent1 <= contentSize1) { - return [ - `0 1 ${contentSize0}px`, - `0 1 ${projectedSizeOfContent1}px`, - '1', - ]; - } - return null; -} diff --git a/test/unit/reducers/ui.js b/test/unit/reducers/ui.js index babcc39cb8..173f2ac523 100644 --- a/test/unit/reducers/ui.js +++ b/test/unit/reducers/ui.js @@ -12,7 +12,6 @@ import { updateProjectInstructions, } from '../../../src/actions/projects'; import { - dragColumnDivider, userDoneTyping, focusLine, editorFocusedRequestedLine, @@ -61,25 +60,6 @@ function withNotification(type, severity, payload = {}) { const gistId = '12345'; -test('dragColumnDivider', reducerTest( - reducer, - initialState, - partial(dragColumnDivider, { - dividerWidth: {minWidth: 4}, - columnWidths: [ - {width: 400, minWidth: 300}, - {width: 400, minWidth: 300}, - ], - deltaX: 5, - lastX: 400, - x: 405, - }), - initialState.setIn( - ['workspace', 'rowFlex'], - new Immutable.List(['0 1 405px', '1', '1']), - ), -)); - test('startEditingInstructions', reducerTest( reducer, initialState,